Repository navigation
Amendment 15 tooling: a lane name is ONE name under any case — canon_lane gives every <lane> argument and LANES_LANE the register row's spelling before any read or write, and a lane's object log is ONE file and ONE lane's under any case - #41
Conversation
…case, and `canon_lane` hands every `<lane>` argument and `LANES_LANE` the register row's own spelling before anything is read or written under it — the row lookup, `lane-start`'s two resolutions and its refusal, `add-row`'s refusal, and the log file that is `git mv`d to the canonical name inside the write's own commit WIP: the four commands are built and the existing suite is green against them; the new `== Amendment 15 ==` test section and the manual are still to come. WHAT IS BUILT (#31 acts 1–4; the amendment is in force, ratified by Brett Heap 2026-09-14T00:07:18Z): * `lanes-edit.sh canon_lane` (`:2632`) — the register row whose key equals the typed name case-insensitively, read from `origin/<branch>` like every other state read (R19): one row prints its OWN spelling, none prints the typed spelling so `add-row` can still create a lane, two or more REFUSE (2) naming them all and citing 15(d). It RETURNS rather than exits, for `row_line`'s reason — every caller takes it with `$( )`. Exposed as the subcommand `canon-lane` (`:6033`) so that `lane-start`, `lane-end`, `lanes` and `restart` share ONE resolver rather than four. * `row_line` (`:691`) and `ROW_AWK` (`:2565`) compare `tolower`. The line an editor rewrites is still the exact line; what changed is how it is FOUND. Two case-differing rows in the working tree are the same refusal. * Every `<lane>` argument and `LANES_LANE` passes through `canon_lane` before any read or write: log, claim, release, who --lane, live-holder, idle-holders, forks, lane-objects, register-row, lane-dir, lane-profile, last-session, lanes --lane, append-session-id, append-row-status, replace-in-row, add-row, commit, append-line, verify-row, resolve-home. The lines written, the commit subjects `LANES(<lane>@ws)` / `LOG(<lane>@ws)` and `canon_object`'s `lane:<name>` key all carry the canonical spelling. * The log file is `lanes/log/<canonical>.md`: `log_files_named_ci` and `log_path_ci` find a file of another case (the real register's historical `openxfactory-2.md` under row `openXfactory-2`), and `ensure_log` renames it ONCE, through a temporary name so a case-insensitive filesystem — macOS, which this repository's CI runs — renames rather than answering "identical". `ensure_log` moved BELOW `handle_preexisting` in `write_event` so the rename is this write's own act and not something the pre-existing capture commits as a peer's; the old path joins the write's pathspecs, so git records the rename in the same commit as the appended line. Measured in a sandbox: `R066 lanes/log/openxfactory-2.md lanes/log/openXfactory-2.md` inside `LOG(openXfactory-2@Eagle): RESUMED …`, and the second write renames nothing. * `lane-start`: `<repo>` resolves to the checkout's real directory name under `$PROJECTS_ROOT` case-insensitively (`:672`), two directories differing only by case refusing and naming both; the lane resolves through `canon-lane` (`:721`); `rows_for_lane` compares `tolower` (`:1068`) so the pre-check RESUMES the row it used to miss instead of reaching `add-row`, and the two-rows refusal now says which spellings and cites 15(d). The window rename and every `--name` take the canonical spelling because `$LANE` is it. * `lane-end`, `restart`: the same resolver on their `<lane>` argument; `restart`'s `awk '$1 == l'` on the swap record compares `tolower`, as does `lane-start`'s. * The SessionStart hook already compared window name, session name and row key case-insensitively and already printed the row's spelling; the block now says so, and carries the one-line marker where Amendment 12's lock plugs in (#25, not built here). EVIDENCE: `bash tests/test_lane_helpers.sh` — 1500 passed, 0 failed, 0 skipped, 0 pending, both before this commit and after it, so nothing that was green went red. `bash -n` on all four files. The new section and `python3 -m pytest tests` follow in the next commit. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…5 ==` section and the manual's naming rule as a predecessor left them uncommitted — the lowercase resume, `add-row`'s case-duplicate refusal, the pair every writer names, and the log file renamed once inside the write's own commit WIP, COMMITTED AS FOUND AND NOT YET MEASURED. This is the working tree `cfa4f51` left dirty, taken verbatim so that nothing of it is lost before the acts it belongs to are finished. It has not been run: the counts in `cfa4f51` (1500 passed, 0 failed) were measured against the four commands with NO new section, and this section's cases are asserted against nothing yet. WHAT IS HERE (#31 acts 5 and 6, partial): * `tests/test_lane_helpers.sh` (+186) — a section `== Amendment 15: a lane name is one name under any case ==` placed immediately before `== the workstation seam ==`, which stays second-to-last for its own reason. It seeds the incident's own shape (a mixed-case row `repoCase-1`, a checkout directory spelled that way, and a log file already written as `repocase-1.md`) and the pair 15(d) merges (`repoPair-1` / `repopair-1`), then asserts: a lowercase `lane-start repocase 1` RESUMES the row and renames the window to the row's spelling; the log is renamed once, inside the write's own commit, which names both paths; `add-row` refuses the case-duplicate naming the row that is there; `log`, `claim` and `append-session-id` each refuse the pair naming both; Rule 6 attribution with a lowercase `Lane:` still matches; `who --lane`, `lanes --lane`, `register-row`, `canon-lane`, `restart` and `lane-end` all take the lowercase spelling and answer with the row's. * `docs/README-lanes.md` (+38/−5) — the naming rule under "Starting and ending a lane", the writer paragraph's first act, `lane-start <repo> <n>`'s two resolutions, its step 5 `add-row`, and the object log's row in the table. WHAT IS NOT DONE: the section has never been run, so the two dirs case, the seeded pair and the rename assertions are claims rather than measurements; `tests/test_repo_hygiene.py`'s DATED line cap on `docs/README-lanes.md` has not been extended for the prose above; AGENTS.md is untouched; no `pytest` run. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, a transcript TITLED in another case is still this lane's and is resumed rather than replaced, `lane-end` reads the register the way `lane-start` reads it, `window-lane` answers from its RECORD rung in the row's spelling, and `--confirm` stops calling this lane's own window not the lane (acts 5 and 6)
Amendment 15's tooling, finished: the section the predecessor wrote had never
been run, and running it found one assertion that was wrong about this code,
five comparisons this code was wrong about, and a refusal that would have
killed `lane-start` on the macOS job.
WHAT THE RUN FOUND, AND WHAT IT CHANGED.
* ONE ASSERTION WAS WRONG AND IS NOW RIGHT ABOUT THE ORDER. The section
expected `lane-start repocase 1` to print `lane repocase-1 → repoCase-1`.
It does not, and should not: `<repo>` is resolved to the checkout's real
directory BEFORE `$LANE` is built out of it (15(a)), so for a lane whose
row and whose checkout are spelled alike the repo half has already produced
the canonical name and the lane half has nothing left to change. Measured:
`lane-start: repo repocase → repoCase (Amendment 15(a): …)`. The assertion
now reads the half that actually spoke, and the PURE lane resolution — the
row's spelling winning over the directory's — is asserted on a new fixture
where only the register can settle it: `repoMix` on disk, `repoMIX-1` in
the register, `--name repoMIX-1`, `rename-window repoMIX-1`, and the row's
own stamp still recording `dir …/projects/repoMix`, the directory's
spelling, which the row does not carry.
* THE OBJECT LOG'S LANE FIELD WAS STILL COMPARED BYTE FOR BYTE (act 1). The
amendment lists the object log among the places a name is looked up, and a
log is APPEND-ONLY: its lane field carries whatever was typed on the day,
so `lanes/log/openxfactory-2.md` holds `lane openXfactory-2` lines from
2026-09-02 beside the `lane openxfactory-2` lines of the 23:51:56Z
incident, in one file, about one lane. `claim_is_stale` and `release`
already lowercased both sides; `lane_objects`, `superseded_by` and
`lane_row_facts` did not, so `who --lane` reported that lane as holding
nothing, `lane-end` ended it without a word, and the `lanes` listing
emitted TWO rows under one lower-cased key and took the first — the state
of one line and the dir, profile and window of the other. All three now
compare `tolower` with the key computed once in `BEGIN`, and each keeps the
SPELLING of the line that won the position test, because those names are
printed (`lane_objects:1861` `on[]`, `superseded_by:1913` `nm[]`,
`lane_row_facts:3724` `disp[]`).
* `window-lane`'s SECOND RUNG ANSWERED WITH THE RECORD'S SPELLING (act 4).
Rung 1 reads the window's name through `lane_named_ci` and has always
answered with the row's; rung 2 reads the swap RECORD, whose lane column
carries whatever the lane was PAUSED under. Its three callers hand what it
prints to `tmux rename-window`, to `--name` and to `restart <lane>`. It
goes through `canon_lane` now and keeps the record's own spelling where the
register holds 15(d)'s pair, because this read is a rung in front of a
launch and its contract is 0, 8 or 64 — the refusal belongs to the writer
it leads to (`lanes-edit.sh:3940`).
* THE TRANSCRIPT TITLE WAS STILL MATCHED BYTE FOR BYTE (act 2), in the one
branch that decides RESUME versus MINT A NEW CONVERSATION. The amendment
names the SESSION NAME among the lookups, `live_holder` and `fork_map` have
matched titles case-insensitively all along, and `lane-start`'s
`titled_transcript` was the one that had not. `openXfactory-2` is in exactly
that state right now: the launcher titled its session `openxfactory-2` on
2026-09-13 and the row has said `openXfactory-2` since the 15(d) merge, so
the next `lane-start` that cannot resolve the row's recorded id in that
checkout would have found NO transcript for the lane and started a SECOND
conversation on it — the incident itself, one branch along. `tolower` on
both sides, the lane lower-cased once outside the loop (`lane-start:1639`).
And the note below it is three answers now instead of two (`:1696`): a
dedupe suffix means another holder was live at the rename and is cured by
retiring it, while a title differing only by CASE is this lane's own
transcript and has nothing to retire — telling a person to hunt stale
holders for the second would send them after a collision that never
happened.
* AND `--confirm` TOLD A PERSON THEIR OWN LANE'S WINDOW WAS NOT THE LANE
(act 4). A window named `openxfactory-2` under the row `openXfactory-2` got
*"named 'openxfactory-2', which is not the lane"* in the sentence the
operator is asked to answer y or N to. The rename still happens either way —
what a wrong sentence changes is the ANSWER, and the reasonable answer to
that one is N, which abandons a resume that was legitimate. Three arms now
instead of two, and the new one says which lane it is and what it will be
renamed to (`lane-start:1508`).
* `lane-end`'s OWN ROW SCAN WAS STILL BYTE FOR BYTE (act 3), under the same
function name whose twin in `lane-start` the same WIP had made `tolower`.
The shared resolver is not the whole of act 3: `canon-lane` spends 2 both on
a register holding 15(d)'s pair AND as the unknown-subcommand code of every
helper predating this amendment, and neither `lane-end` nor `restart` can
tell those apart — so both carry on with the name AS TYPED, and what the
command's own read of the register then says is the answer a person gets.
Byte for byte, `lane-end openxfactory-2` on the lane `openXfactory-2` read
as NO ROW, and that refusal's own remedy is `add-row` — the 23:51:56Z
incident, performed by the other end of the lane. (`add-row` refuses it, so
the register was safe either way; the MESSAGE was the defect.) `tolower` on
both sides now, and the two-rows refusal names the spellings and cites 15(d)
as `lane-start`'s does (`lane-end:651`, `:679`). Asserted directly, because
it is directly observable: `lane-end repopair-1` on the seeded pair now
refuses 2 naming both, where it used to find exactly one row and go on.
* A REFUSAL THAT WOULD HAVE KILLED `lane-start` ON macOS. The two-rows
refusal acts 1-4 wrote reads `"spelled $rfl_names— a lane name is ONE
name…"`, and bash decides where a variable NAME ends with `isalnum()`,
which is LOCALE-DEPENDENT: bash 3.2 on Darwin under the runner's
`en_US.UTF-8` reads the three bytes of the em dash as name characters, looks
up `rfl_names—`, and `set -u` kills the script — in the one branch whose
whole job is to explain a register a person has to repair by hand. `bash -n`
cannot see it and neither can a Linux run;
`test_no_shipped_bash_leaves_a_variable_name_to_bash_3_2s_locale` did, the
first time this branch was put in front of it. Braced (`lane-start:1111`).
THREE SMALLER THINGS, IN THE SAME PASS.
* `lanes --lane`'s resolver loop took `${#lns_args[@]}` and an index. An
EMPTY array under `set -u` is the case `${arr[@]+"${arr[@]}"}` exists for on
bash 3.2 — the idiom the `lanes_rows` call one line below already uses —
and a bare `lanes` with no flags is that case. Rebuilt with it, and it no
longer assumes the `--lane` value sits at `i + 1` either.
* `ensure_log`'s rename refusal said *"move it by hand"* without saying WHICH
PATH. Its two `mv`s fail differently: the first leaves the log exactly where
it was, the SECOND leaves it at `<log>.amendment15.<pid>`, a name no reader
of that directory looks for. Split in two, each naming where the file
actually is — this is a refusal that costs a lane its whole history if it is
acted on wrongly.
* The manual said the log file is `git mv`d. It is not, and the code says why
one line along: `git mv a A` is not a rename on a case-insensitive
filesystem. Two plain `mv`s through a temporary name, inside the write's own
commit, which is what makes git record it as a rename.
THE SUITE (act 5). The section is `== Amendment 15: a lane name is one name
under any case ==`, immediately before `== the workstation seam ==`, which
stays second-to-last. THIRTY-THREE assertions were added to the ones inherited,
and FOUR fixtures with them, each the shape of a real defect rather than a
contrivance:
* `repoHold-1`, whose log carries a STARTED spelled the row's way and a
CLAIMED spelled the other. Matched byte for byte that hold belongs to no
lane at all, so `who --lane` would say this one holds nothing and `lane-end`
would end it without a word. Asserted both ways round: `who --lane` names
`opensoft/repoHold#9`, and `lane-end` REFUSES, naming it and the one word
that ends the lane anyway.
* `repoMix` on disk and `repoMIX-1` in the register, where the directory and
the row disagree and neither answer can be standing in for the other.
* `repoWin-1`, whose dir and profile are on a STARTED spelled the row's way
and whose state and swap record are on a PAUSED spelled the other — so
column 10 of `lanes --lane repowin-1` is `restart repoWin-1` only if both
halves are read as ONE lane's, and `window-lane Raven casewin:3` answers
`repoWin-1` only if the record's spelling is resolved.
* `repoTitle-1`, whose row names an id with no transcript here and whose
transcript is TITLED `repotitle-1` — the state `openXfactory-2` is in. The
launch must be `--resume repoTitle-1` with no minted `--session-id` at all,
which is the difference between resuming the lane's conversation and
starting a second one; and `--confirm`, run in a window named
`repotitle-1`, must say which lane that window is rather than that it is not
the lane.
THE HOLD IS ON A LANE OF ITS OWN AND NOT ON `repoCase-1`, deliberately: the only
line of `who --lane`'s output that carries a lane NAME is the one it prints when
the lane holds nothing, and an inherited assertion reads exactly that line. Put
on `repoCase-1`, the new fixture would have taken that line away and made a
green assertion red for a reason that is not about this amendment. Nothing
inherited was weakened to make room for it.
THE MANUAL (act 6). `docs/README-lanes.md` gains the lane field's case in the
object-log grammar and the canonical answer in `window-lane`'s row of the reads
table, beside the naming rule, the writer's first act, `lane-start <repo> <n>`'s
two resolutions and the log file's name that were written with acts 1–4.
AGENTS.md is UNTOUCHED and that is the reading: its lane paragraph says where
the four commands find the register and whose exit codes they spend, and this
amendment changes neither — so its 224-line cap in `tests/test_repo_hygiene.py`
does not move either. `docs/README-lanes.md` carries no cap.
EVIDENCE, on this workstation (Linux, a case-SENSITIVE filesystem, so the
two-directories case ran rather than skipping).
`bash tests/test_lane_helpers.sh` on the tree this commit's parent leaves:
**1563 passed, 1 failed, 0 skipped, 0 pending** — the one failure the wrong
assertion above. On this tree: **1597 passed, 0 failed, 0 skipped, 0 pending**. `python3 -m pytest tests -q`:
**549 passed** in 866 s, which runs that same suite a THIRD time through
`tests/test_lane_helpers_suite.py` — on the exact tree this commit records — and
asserts its footer says `0 failed`.
`bash -n` on every file touched, and the static rules in
`tests/test_repo_hygiene.py` on every edit as it was made — which is how the
macOS refusal above was found. No assertion was weakened or deleted; the one
that changed is named above, with what was measured in its place.
Lane: openrepotools-3 (openRepoTools-3)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.
You can request another review in 1 day and 15 hours by commenting @sourcery-ai review. Upgrade to get a review now.
Reviewer's GuideAmendment 15 makes lane identity case-insensitive everywhere while treating the register row's spelling as canonical, preventing duplicate rows and sessions and consolidating each lane's object log into one canonical file; the PR centralizes resolution, updates all affected tooling and readers, handles legacy case mismatches safely, and adds broad compatibility and regression coverage. Sequence diagram for case-insensitive lane resolution and canonical writessequenceDiagram
participant Caller
participant LaneTools as lanes-edit.sh
participant Register as PublishedRegister
participant Log as ObjectLog
participant Git
Caller->>LaneTools: canon-lane typedLane
LaneTools->>Register: rows_named_ci typedLane
Register-->>LaneTools: zero, one, or multiple matches
alt one matching row
LaneTools-->>Caller: canonical row spelling
Caller->>LaneTools: write operation with canonical lane
LaneTools->>Log: ensure_log canonical lane
Log-->>LaneTools: existing case-variant log or canonical path
LaneTools->>Git: commit canonical write and old log path
else no matching row
LaneTools-->>Caller: typed lane spelling
Caller->>LaneTools: add-row
else multiple case-variant rows
LaneTools-->>Caller: refusal naming all spellings
end
Sequence diagram for resuming a lane typed in another casesequenceDiagram
participant User
participant LaneStart as lane-start
participant LaneTools as lanes-edit.sh
participant Register
participant Transcript
participant Tmux
User->>LaneStart: lane-start typedRepo typedLane
LaneStart->>LaneTools: canon-lane typedLane
LaneTools->>Register: rows_named_ci typedLane
Register-->>LaneTools: canonical lane spelling
LaneTools-->>LaneStart: canonical lane
LaneStart->>Register: rows_for_lane canonical lane
LaneStart->>Transcript: case-insensitive title lookup
Transcript-->>LaneStart: existing conversation
LaneStart->>Tmux: rename-window canonical lane
LaneStart->>Transcript: resume existing conversation
LaneStart-->>User: resumed lane, no second row
Entity relationship diagram for canonical lane identity and log storageerDiagram
REGISTER_ROW {
string lane_name PK
string session_ids
string directory
}
OBJECT_LOG {
string canonical_filename PK
string lane_field
string event
}
REGISTER_ROW ||--|| OBJECT_LOG : owns
REGISTER_ROW {
string lane_name "case-insensitive identity; row spelling is canonical"
}
OBJECT_LOG {
string lane_field "case-insensitive matching; historical spelling preserved"
}
Flow diagram for canonical object-log migrationflowchart TD
A[Write lane event] --> B[Resolve register spelling]
B --> C{Case-variant log exists?}
C -->|No| D[Use canonical log path]
C -->|One| E[Rename through temporary path]
E --> F[Stage old and canonical paths]
F --> G[Append event]
D --> G
C -->|Multiple| H[Refuse and name all log files]
G --> I[Commit rename and write together]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
🟡 Changes recommended
Moderate unresolved issues remain in directory overrides, case-insensitive state grouping, log lookup and migration, --all handling, and ambiguous-row attribution.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR enforces case-insensitive lane identity using the register’s canonical spelling across tooling, logs, sessions, and windows.
Changes:
- Adds shared lane canonicalization and duplicate detection.
- Updates lifecycle commands, log handling, and state reads.
- Expands Amendment 15 tests and documentation.
File summaries
| File | Summary |
|---|---|
tests/test_lane_helpers.sh |
Adds comprehensive Amendment 15 integration coverage. |
restart |
Updates canonical lane and swap-record handling. |
lanes-edit.sh |
Implements lane resolution, log migration, and state handling. |
lane-start |
Updates repository, lane, transcript, and session resolution. |
lane-end |
Adds canonical lane ending and duplicate-row handling. |
docs/README-lanes.md |
Documents canonical lane naming and log behavior. |
Review details
Suppressed comments (7)
lane-start:830
- The case-insensitive comparison at this call site is too late:
swapped_candidatesgroups lane-kind records by the raw$2. If one log hasPAUSEDunder the old case followed byRESUMEDunder the canonical case, it still emits the oldPAUSEDas a swap candidate, so this path can use a stale swap directory/profile/window. Group the source records bytolower($2)before selecting each lane's last line.
ld_row="$(printf '%s\n' "$ld_out" | awk -F'\t' -v l="$LANE" 'tolower($1) == tolower(l) { print; exit }')"
lanes-edit.sh:1326
git ls-treewithout-rdoes not enumerate files beneath thelanes/logtree. When a published log exists only under another case,log_path_citherefore falls back to the canonical path andlane_log_events/lane_log_existsreport no log until a write migrates it, contrary to the reader contract. Recurse when looking up the log files.
lpc_hit="$(git -C "$LANES_REPO" ls-tree --name-only "origin/$LANES_BRANCH" -- "$LANES_LOG_PREFIX" 2>/dev/null \
| awk -v want="$lpc_exact" 'BEGIN { w = tolower(want) } tolower($0) == w { print; exit }')"
lanes-edit.sh:1342
- This migration only searches the working tree, while the state readers deliberately support logs that exist on
origin/<branch>but have not been pulled locally. On a stale checkout with onlylanes/log/repocase-1.mdpublished,el_hitsis empty, so the write createsrepoCase-1.md; the later pull can then leave both case variants instead of performing the required one-time rename. The remote old-case path must be materialized/handled before creating the canonical file.
el_hits="$(log_files_named_ci "$lane")"
el_n="$(printf '%s' "$el_hits" | grep -c . || :)"
lanes-edit.sh:3941
- Canonicalizing the spelling after selecting the swap record does not merge case variants in the source history:
swapped_candidatesstill indexes its last lane-kind line by the raw$2. APAUSEDline forrepoWin-1followed by aRESUMEDline forrepowin-1therefore leaves the older swap candidate alive, so this read can bind a stale window reference. The candidate map needs a lower-cased key, while preserving the original spelling only for display before this resolver returns the row spelling.
wl_can="$(canon_lane "$wl_l" 2>/dev/null || :)"
printf '%s\n' "${wl_can:-$wl_l}"
lanes-edit.sh:6019
- This resolves
--lanebeforelanes_rowsapplies its documented--alloverride. Withlanes --all --lane repopair-1, a register containing the Amendment 15 duplicate pair now exits 2 fromcanon_lane, even though--allis explicitly supposed to clear--laneand list every lane. Track whether--allwas supplied and skip the resolver for an ignored selector (regardless of flag order), or reject the contradictory combination before canonicalization.
lns_new=(); lns_take=0
for lns_a in ${lns_args[@]+"${lns_args[@]}"}; do
if [ "$lns_take" = 1 ]; then
lns_take=0
lns_one="$(canon_lane "$lns_a")" || exit 2
lns_new+=("$lns_one")
lanes-edit.sh:5259
row_lane_cireturns an empty string unless exactly one row matches. For the Amendment 15 case-duplicate pair, this leaveslane_tagempty andappend-lineproceeds with aLANES(unknown@...)commit instead of refusing the ambiguous writer operation, losing Rule 6 attribution. Resolve the extracted candidate throughcanon_laneso its 15(d) refusal is propagated.
[ -n "$cand" ] && lane_tag="$(row_lane_ci "$cand")"
restart:418
- This comparison only makes the consumer match a swap record case-insensitively;
swapped_candidatesstill keys its last lane-kind event by the raw$2(lanes-edit.sh:4594). A historicalPAUSEDunderrepowin-1followed byRESUMEDunderrepoWin-1therefore produces a stale PAUSED candidate, andrestartcan relaunch a lane that is no longer paused. Normalize the producer's key too while retaining its original spelling for display.
record_row="$(printf '%s\n' "$sw_out" | awk -F'\t' -v l="$lane" 'tolower($1) == tolower(l) { print; exit }' || :)"
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…Y where the index matches the path EXACTLY, a lane met its own hold as a rival at both ends of `claim`, a `PAUSED` outlived the `RESUMED` that ended it, `--all` refused for the selector it throws away, a Rule 6 line was attributed to nobody, and a refusal refused the remedy it prints
THE macOS JOB WENT RED ON THE FIRST PUSH AND IT WAS RIGHT TO. `tests-macos` at
`48f2111`: **1570 passed, 15 failed, 7 skipped**, every one of the fifteen in
the Amendment 15 section, and the first of them is the whole of it —
*"the rename landed in the WRITE's own commit, not a commit of its own"*, whose
`git log -1 -- lanes/log/repoCase-1.md` came back EMPTY.
* ON A CASE-INSENSITIVE FILESYSTEM GIT'S INDEX MATCHES THE TWO NAMES AS ONE.
`ensure_log` renames the log on disk — two `mv`s through a temporary name,
which is right on both platforms — and hands the old path and the new one
to `commit_push`. There, `git add -- lanes/log/repocase-1.md
lanes/log/repoCase-1.md` found ONE path: the file is present under the
index's own spelling as far as git is concerned, so it staged a
MODIFICATION of the old name and no rename reached the commit at all. The
writes behind it then met a checkout git called modified and refused (2),
and the one after that could not rebase (3) — which is the cascade the
other fourteen are. `commit_push` stages and commits its own paths with
`-c core.ignorecase=false` now (`lanes-edit.sh:1027`): exact matching makes
the old path a file the index has and the worktree does not — a DELETION —
and the new path a file nothing in the index claims — an ADDITION. One
commit, one rename, on either kind of filesystem. It narrows nothing else:
every path this function is given is one this file computed itself.
TESTED ON LINUX, because `core.ignorecase` is a CONFIG git sets from that
probe and not the probe itself: the sandbox's own register repository is
set `core.ignorecase true` for one write and unset after it, which
reproduces the index half exactly. Without the fix that write leaves
`git log -- <the canonical path>` empty; with it the commit names both
paths and the checkout is clean behind it.
* AND A LANE LOST THE RACE TO ITSELF. Found by this round's own new case
rather than by review: `claim_rescan_hook` — the read `commit_push` calls
after every rebase, which decides whether a peer's claim landed first —
excluded THIS lane and the lane being taken over byte for byte, exactly as
`claim`'s pre-check did. Measured: exit **7**, CLAIM-LOST, with the lane
itself named as the winner and a `CLAIM-LOST` line written into its own
log. Both exclusions are `grep -iv` now (`lanes-edit.sh:5118`).
COPILOT ROUND 1 — SIX TAKEN, ONE DECLINED WITH A MEASUREMENT. Two were inline
comments and five were in its `<details>` block; three of those five are ONE
defect at three call sites and are fixed once, in the producer.
* `lane-start` REFUSED THE REMEDY IT PRINTS. The two-directories refusal
names `--dir <path>` as the way out and ran BEFORE the rung that honours
it: `lane-start --dir /chosen repotwin 1` was rejected for an ambiguity the
flag had already settled. It is now for a run that has no other answer;
with the checkout named, what is left is ambiguous only about the lane's
NAME, so the run SAYS that, keeps the typed `<repo>`, and lets the register
row's spelling overrule it below (`lane-start:690`).
* A LANE MET ITS OWN HOLD AS A RIVAL. `PER_LANE_AWK` is the one-row-per-lane
reduction every state read is built on, and it keyed on the raw lane field:
a log spelling its lane two ways emitted TWO rows for ONE lane. `claim`
removes its own row from the holders of the object it is claiming, so the
other spelling stayed in the set and the lane was refused its own object —
*"lane repohold-1 holds opensoft/repoHold#9"*, about itself. Keyed on
`tolower` with the winning line kept whole, so what a reader sees is still
a spelling somebody wrote (`lanes-edit.sh:1830`), and `claim`'s own
exclusion is `grep -iv` for the same reason (`:5505`).
* A `PAUSED` OUTLIVED THE `RESUMED` THAT ENDED IT. `swapped_candidates` keyed
its last-lane-kind-line map the same way, so a lane PAUSED under one
spelling and RESUMED under the other kept the swap candidate alive for
ever. That map is the producer for all three of `restart`'s record read,
`lane-start`'s directory rung and `window-lane`'s second rung — which is
why one fix closes three of the report's findings: `restart` would relaunch
a lane that is running, and `window-lane` would bind a window ref the lane
has since left. Keyed on `tolower`, with `nm[]` carrying the spelling the
winning line used, because this read PRINTS it (`lanes-edit.sh:4641`).
* `--all` REFUSED FOR THE SELECTOR IT THROWS AWAY. `lanes_rows` clears
`--lane` under `--all` — its own comment calls that absolute — while the
resolver above it ran on the `--lane` value first, so `lanes --all --lane
<the 15(d) pair>` exited 2 and listed NOTHING, for a name the run was about
to ignore. The resolver is skipped for an `--all` run, in either flag order
(`lanes-edit.sh:6089`).
* A RULE 6 LINE WAS ATTRIBUTED TO NOBODY. `append-line` reads its lane out of
the line's own text through `row_lane_ci`, which answers EMPTY for two rows
exactly as it does for none — so the one register state every writer is
supposed to refuse produced a commit attributed to no lane at all.
`canon_lane` is now asked for its STATUS there and its 15(d) refusal
propagates, while which of *no row* and *one row* it is stays
`row_lane_ci`'s answer (`lanes-edit.sh:5321`).
* A WRITE ON A STALE CHECKOUT WOULD HAVE SPLIT ONE LOG IN TWO. `ensure_log`
scans the WORKING TREE for a file of another case, while every state reader
here answers out of `origin/<branch>` (R19): on a checkout that is behind,
a published `lanes/log/repocase-1.md` is invisible to that scan, this write
creates `repoCase-1.md`, and `commit_push`'s own rebase then lands BOTH —
curable only by 15(d)'s hand merge. It refuses instead, naming the
published path and the one command that fixes it (`lanes-edit.sh:1375`).
Copilot asked for the remote path to be materialised here; that is `git
pull` written a second time inside a function whose whole contract is one
lane's file, and a refusal a person can act on is the smaller thing.
DECLINED, WITH WHAT WAS MEASURED. *"`git ls-tree` without `-r` does not
enumerate files beneath the `lanes/log` tree"* — it does, because the pathspec
`$LANES_LOG_PREFIX` ENDS IN A SLASH (`lanes-edit.sh:1219`,
`${LANES_PREFIX}log/`), and the finding quotes the call without it. On the real
register: `git ls-tree --name-only origin/main -- lanes/log/` prints
`lanes/log/README.md`, `lanes/log/browser-ui-repair.md`,
`lanes/log/codeXfactory-1.md`, …; the same command with `lanes/log` and no
slash prints the single tree entry `lanes/log`; and `-r` adds nothing to the
first. `log_path_ci` finds a published file of another case as written.
TESTED, EACH ONE DIRECTLY. The case-insensitive index, above; `--dir` past the
two-directories refusal (inside the case-sensitive-filesystem branch, which is
where that pair can exist at all); `claim` on an object its own log holds under
the other spelling exits 0, names the lane against itself at neither end, and
does not lose the race to itself; a new `repoSwap-1` whose log is PAUSED as
`reposwap-1` and RESUMED as `repoSwap-1` is NOT a swap candidate, so
`window-lane Raven swapwin:4` answers 8 and names nothing; `lanes --all --lane`
on the seeded pair lists every lane instead of refusing; and `append-line` on a
line naming that pair refuses 2, cites 15(d) and writes no line.
ONE ASSERTION OF THIS ROUND'S OWN WAS TOO BROAD AND IS NARROWED, said here
because it went red once on the way: `claim`'s new case forbade the substring
*"holds opensoft/repoHold#9"*, which is in the line a SUCCESSFUL claim prints —
*"lane repoHold-1 now holds …"* — as well as in the refusal. It now forbids the
two failures it exists for by their own words: the pre-check's *"Rule 1: the
lane stops and reports"* and the post-rebase read's *"CLAIM LOST"*.
EVIDENCE. `bash tests/test_lane_helpers.sh`: **1616 passed, 0 failed, 0
skipped, 0 pending**, against 1597 on the first push — nineteen assertions
added, none removed.
`python3 -m pytest tests -q`: **549 passed** in 877 s, on the exact tree this
commit records. `python3 -m pytest tests/test_repo_hygiene.py -q`: 123 passed,
on every edit as it was made. `bash -n` on both files touched. CI at `48f2111`: `tests` **pass**
(10m05s), `tests-no-submodule` **pass** (9m38s), `tests-windows` **pass**,
SonarCloud **pass**, `tests-macos` **FAIL** (22m11s) — the job this commit is
for.
Lane: openrepotools-3 (openRepoTools-3)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Published/local-state handling, ambiguous logs, and refusal diagnostics still have unresolved issues.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (8)
lane-end:676
- This scan is local-only even though the resolver immediately above reads the published register. When a checkout is behind and
origin/mainhas the row but$LANES_FILEdoes not, this branch reports “no row” and recommendsadd-row; following that remedy can create the duplicate row that the published read already proved exists. Distinguish a stale checkout and instruct the operator to pull/rebase before ending the lane.
row="$(rows_for_lane "$LANE" || :)"
row_count=0
[ -n "$row" ] && row_count="$(printf '%s\n' "$row" | grep -c . || :)"
case "$row_count" in
lane-start:741
- An upgraded helper uses exit 2 for the actual Amendment 15(d) ambiguity, but this branch treats
2as a silent compatibility case and discards the resolver's stderr. If the duplicate rows are present onorigin/mainbut not in this checkout, the laterregister-rowalso has its stderr suppressed andpub_rc=2falls into the generic failure path, solane-startnever names the two spellings or reports the refusal as exit 2. Relay this diagnostic (or explicitly handlepub_rc=2) rather than swallowing it.
cl_out=""; cl_rc=0
cl_out="$("$LANES_EDIT" canon-lane "$LANE" 2>/dev/null)" || cl_rc=$?
case "$cl_rc" in
0)
if [ -n "$cl_out" ] && [ "$cl_out" != "$LANE" ]; then
step "lane $LANE → $cl_out (Amendment 15: the register row's spelling is canonical)"
LANE="$cl_out"
fi ;;
2) : ;;
*) note "$LANES_EDIT canon-lane $LANE failed (exit $cl_rc) — carrying on with the name as typed, which the register may spell differently" ;;
lanes-edit.sh:1349
- When there is no
origin/<branch>ref, this fallback returns the canonical path without checking the local case-variant filename.claimuseslog_path_cias its dirty-check path beforeensure_log; with a localrepoCase-1.mdandLANES_LANE=repocase-1, an uncommitted edit to that target is reported as an unrelated dirty file and the claim is refused, so the first-write migration cannot run. The no-remote fallback should return the matching local log as a repository-relative path before defaulting tolpc_exact.
printf '%s\n' "$lpc_exact"
lanes-edit.sh:1378
- This guard only checks the published filename when
el_n=0. If a migration commit was created locally but its push failed, the next run can have the canonical file locally whileoriginstill has the old-case file;el_nis then 1, so the check is skipped and the append can be rebased/pushed with both paths. Run this published-path check whenever a remote ref exists, regardless of the local hit, and refuse until the checkout has the published log state.
if [ "$el_n" = 0 ] && have_remote_ref; then
el_pub="$(log_path_ci "$lane")"
if [ "$el_pub" != "$(log_path_for "$lane")" ]; then
die "lane $lane-s object log is published as $el_pub and this checkout does not have it: the row spells the lane $lane, so the log is $(log_path_for "$lane") (Amendment 15), and writing one here now would leave TWO files for one lane. Pull first — \`git -C $LANES_REPO pull --rebase\` — and re-run; the rename then happens once, inside this write-s own commit. Nothing was written." 2
lanes-edit.sh:5352
add-rowonly checks the working-tree register. If this checkout is behindorigin/main, the published register can already contain the same lane whilerows_named_ci_localreturns no match;commit_pushthen rebases the new row on top and can recreate the case-duplicate this amendment is meant to prevent. Check the published rows as well as local uncommitted rows (deduplicating the same spelling) before appending.
ar_hits="$(rows_named_ci_local "$lane_new")"
lanes-edit.sh:4643
- These changes preserve the raw spelling from the event line in
sw_l, butswapped_lanesstill callslog_path_for "$sw_l"for the ranking lookup. APAUSEDline such asreposwap-1in the canonicalrepoSwap-1.mdtherefore has no matchinggit log -Sresult on a case-sensitive filesystem, stays at the fallback rank, and can violate the established most-recently-landed ordering when multiple lanes are swapped. Resolve the path case-insensitively (for example withlog_path_ci) before ranking.
{ k = tolower($2); p = pos($10, $11)
if (!(k in mp) || p >= mp[k]) {
mp[k] = p; verb[k] = $3; utc[k] = $1; pay[k] = $8; uuid[k] = $4; ws[k] = $5; obj[k] = $6; nm[k] = $2 } }
lanes-edit.sh:1378
- The refusal text contains two missing apostrophes:
$lane-sexpands to e.g.repoCase-1-s, andwrite-sis likewise printed literally. This makes the actionable “lane's object log” and “write's own commit” guidance hard to read and contradicts the corrected refusal described by the change.
die "lane $lane-s object log is published as $el_pub and this checkout does not have it: the row spells the lane $lane, so the log is $(log_path_for "$lane") (Amendment 15), and writing one here now would leave TWO files for one lane. Pull first — \`git -C $LANES_REPO pull --rebase\` — and re-run; the rename then happens once, inside this write-s own commit. Nothing was written." 2
restart:373
- When the published register contains both case-variant rows,
canon-lanereturns 2 and this path deliberately continues with the typed name, but the subsequentregister-rowcall suppresses its stderr and the*)arm converts that same refusal into a generic exit 1.restarttherefore neither returns the documented refusal code 2 nor names both spellings, unlikelane-endand the other writers. Preserve/relay theregister-rowrefusal (while retaining the compatibility handling for an older helper) instead of treating it as an unknown-helper failure.
cl_out="$(LANES_NO_FETCH=1 "$LANES_EDIT" canon-lane "$lane" 2>/dev/null)" || cl_rc=$?
if [ "$cl_rc" = 0 ] && [ -n "$cl_out" ] && [ "$cl_out" != "$lane" ]; then
note "lane $lane → $cl_out (Amendment 15: the register row's spelling is canonical)"
lane="$cl_out"
fi
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
… of the filesystem, and the new case-insensitive-index case UNSET it rather than putting it back — eleven red assertions on the macOS job, every one of them behind a log file git had been told to stop recognising
A TEST-ONLY FIX, AND THE PRODUCT FIX UNDER IT LANDED. The macOS job across the
two pushes:
`48f2111` 1570 passed, 15 failed, 7 skipped — `git add` could not record a
case-only rename, so `git log -- <the canonical path>` came back
EMPTY and every write behind it met a checkout git called modified.
`e35f2a8` 1590 passed, 11 failed, 7 skipped — `commit_push`'s
`-c core.ignorecase=false` did its job: the rename block, the
row-unlike-its-checkout case, the titled transcript and `--confirm`
are all GREEN there now. What is left is this commit's subject.
WHAT THE ELEVEN ARE. The case added for that fix sets `core.ignorecase true` on
the sandbox's register repository for one write — which reproduces the index
half of a case-insensitive filesystem on a case-SENSITIVE one, and is why the
fix could be tested on Linux at all — and then put the repository back with
`git config --unset core.ignorecase`.
ON macOS THAT SETTING WAS ALREADY THERE AND IS LOAD-BEARING. `git init` probes
the filesystem and WRITES `core.ignorecase = true` into the repository's own
config; unsetting it leaves git treating a case-insensitive filesystem as
case-sensitive. From that point every log this branch had renamed read as a
phantom modification — `lanes/log/repocase-1.md` reported dirty, in the job's
own words — and `refuse_dirty_checkout` refused the writes after it for a
change they did not make. Ten of the eleven are that cascade; the eleventh is
the case itself.
So the case SAVES the repository's own value and restores it, and only falls
back to `--unset` where there was none to begin with — which is the Linux case,
where `git init` writes nothing and the flip is what the test needs.
EVIDENCE. `bash tests/test_lane_helpers.sh`: **1616 passed, 0 failed, 0
skipped, 0 pending** — the SAME count as `e35f2a8`, which is the point: on a
case-SENSITIVE filesystem `git init` writes no `core.ignorecase` at all, the
fallback is the `--unset` this case already did, and nothing here moves. `python3 -m pytest tests/test_repo_hygiene.py -q`: 123
passed. Nothing outside `tests/test_lane_helpers.sh` is touched, and the
assertion count does not move: the same case, put back the way it found things.
Lane: openrepotools-3 (openRepoTools-3)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved correctness, portability, and lane/log handling issues remain.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (11)
Previously missed (5) — in code that hasn't changed since the last review.
lane-end:509
- A published 15(d) pair makes
canon-lanereturn 2, but this caller suppresses that result and continues with the typed spelling. If the local register is stale,rows_for_lanecan then see only one row (or none), so lane-end does not produce the required duplicate-spelling refusal and can report a misleading missing/valid row until the later writer call fails.
lane-start:694 - When
--dirselects one of two case-distinct directories, this branch leavesrepoas the typed spelling. If no row exists,LANEis therefore minted asrepotwin-1even though the selected checkout isrepoTwin; the new rule and the documentation require a new lane's<repo>half to use the checkout's real directory spelling. Setrepofrom the selected--dirbasename for new lanes, while still allowing an existing register row to override the lane spelling.
lane-start:740 canon-lanereturns 2 for the current helper's published 15(d) duplicate-row refusal, but this branch deliberately discards that status. If the duplicate pair exists only on origin while this checkout has no matching local row, the laterregister-rowcall also returns 2 and is mapped to the generic exit-1 helper-failure path, without relaying the two spellings or the Amendment 15(d) cure. Preserve the current-helper refusal while retaining compatibility with genuinely older helpers.
lanes-edit.sh:5488- This dirty-check path is built before
laneis canonicalized. In a local-only/no-origin checkout,LANES_LANE=repocase-1makeslog_path_ciexcludelanes/log/repocase-1.md, so a dirty canonicallanes/log/repoCase-1.mdis reported as an unrelated change andclaimis refused even though it is the lane's own log. Resolve the lane before constructing the exemption path (and keep the later post-sync resolution).
restart:372 - The resolver's 2 is the current helper's published duplicate-row refusal, but this new branch ignores it.
register-rowthen returns 2 for the same state and the following case reports a generic failure (exit 1), sorestartneither gives its normal refusal status nor exposes the two spellings that must be merged.
lane-start:672
- On macOS, the BSD
lsdoes not accept the GNU-style--terminator; its error is discarded here, so this resolver returns no directory matches for every projects root. A case-mismatched<repo>therefore falls through to the incorrectly cased default path, breaking the new cross-case repository resolution on a supported platform.
ls -- "$PROJECTS_ROOT" 2>/dev/null |
lanes-edit.sh:5505
- This pre-check has the same regular-expression bug: a lane containing
.can cause a different lane to be removed fromheldby thegrep -ivpattern. Use an exact comparison of the first US-delimited field so case-insensitivity does not turn distinct lane names into the same holder.
held="$(state_events | lane_states_on "$obj" | holders_of "$obj" | grep -iv "^$lane$US" || :)"
lanes-edit.sh:1761
- The new case-insensitive local lookup can return multiple log files for one lane, but
head -n1silently reads only one of them when no remote ref is available. That drops claims or lane state from the other case variant, even thoughensure_logalready treats multiple matching logs as an actionable corruption; this reader should fail closed or combine all matching files consistently.
ll_f="$(log_files_named_ci "$1" | head -n1)"
[ -n "$ll_f" ] && [ -f "$ll_f" ] || return 0
log_events "$ll_f"
lanes-edit.sh:1346
- When the published register contains both
Lane-1.mdandlane-1.md, thisexitselects whichever pathls-treeprints first. On a case-insensitive checkoutensure_logmay see only one working-tree entry, so the second tracked log is not rejected and per-lane readers can silently lose half the history. Count all case-insensitive remote matches and refuse the ambiguous two-file state before choosing a path.
lpc_hit="$(git -C "$LANES_REPO" ls-tree --name-only "origin/$LANES_BRANCH" -- "$LANES_LOG_PREFIX" 2>/dev/null \
| awk -v want="$lpc_exact" 'BEGIN { w = tolower(want) } tolower($0) == w { print; exit }')"
lanes-edit.sh:4673
- After this change
swapped_candidatesemits the spelling from the PAUSED line (nm[l]), butswapped_lanesstill uses that field as thelog_path_forpathspec. Once a case-only log rename has produced the canonical file, a PAUSED line spelledreposwap-1makes the pickaxe search forlanes/log/reposwap-1.mdon a case-sensitive filesystem, so it misses the swap commit and assigns the fallback rank999999999, misorderingswappedresults. Use the case-insensitive/canonical log path for that history lookup.
nm[l], 31, utc[l], 31, (w == "" ? "unknown" : w), 31,
(d == "" ? "" : d), 31, (pf == "" ? "" : pf), 31,
verb[l], nm[l], uuid[l], ws[l], utc[l], obj[l]
lanes-edit.sh:1378
- This user-facing refusal renders possessives as
lane <name>-sandwrite-s(for example,lane repoCase-1-s), which is misleading and unprofessional. Use shell-safe bracing and apostrophes so the message sayslane <name>'sandwrite'sown commit.
die "lane $lane-s object log is published as $el_pub and this checkout does not have it: the row spells the lane $lane, so the log is $(log_path_for "$lane") (Amendment 15), and writing one here now would leave TWO files for one lane. Pull first — \`git -C $LANES_REPO pull --rebase\` — and re-run; the rename then happens once, inside this write-s own commit. Nothing was written." 2
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Lite
| # `ls` and one `awk`, not a `$(lc …)` per file: `lanes/log/` carries a file per | ||
| # lane of the estate and this is asked on the path a person is waiting on. | ||
| log_files_named_ci() { # <lane> — every existing log file for it, whatever its case | ||
| ls -- "$LANES_LOG_DIR" 2>/dev/null | awk -v want="$1" -v d="$LANES_LOG_DIR" ' |
…tted from it, because `git commit -- <paths>` is `--only` and rebuilds the tree out of a working tree where a case-insensitive filesystem still answers to the old name — the macOS job's eleven red assertions were ONE file on disk and TWO paths in the tree
The diagnosis in `928908a` was wrong and this commit says so: `core.ignorecase`
is `true` on the macOS runner already, so the test setting it was a no-op there
and restoring it changed nothing. The count came back IDENTICAL — 1590 passed,
11 failed, 7 skipped at `e35f2a8` and 1590/11/7 again at `928908a`. The defect
was never the config the suite touched. It is in `commit_push`, and three
things about a case-INSENSITIVE filesystem have to be true at once to see it:
* `git add -- <old> <new>` under git's own `core.ignorecase=true` matches
BOTH pathspecs to the ONE index entry and stages a modification under the
old name. That is `48f2111`: fifteen red, `git log -- <canonical>` EMPTY.
* The same `add` under `core.ignorecase=false` — the fix in `e35f2a8` — is
worse, not better. Exact matching changes what GIT compares; it does not
change what the OPERATING SYSTEM resolves, and `lstat` on
`lanes/log/repocase-1.md` still finds the file now named `repoCase-1.md`.
So git stages the old entry as MODIFIED and the new path as a SECOND entry,
and the commit carries both. One file on disk, TWO paths in the tree — a
lane with two committed logs, which is the state this whole amendment
exists to make impossible. Every checkout after it reports the path it
cannot materialise as deleted, and the writes behind it are refused (2) for
a dirty checkout they did not make: that is all eleven red assertions, and
the refusal each of them actually printed names `lanes/log/repocase-1.md`.
* `git commit -- <paths>` is `--only`. It builds the tree from HEAD plus the
WORKING TREE of those paths and disregards what is staged — so an index
holding the rename exactly is thrown away by the commit anyway, and the old
path comes back. No amount of care in `add` can survive it.
So the old entry is dropped from the INDEX by its exact path, where no
filesystem is consulted at all; the new path ALONE is added — `git add` fails
on a pathspec matching nothing, and after the force-remove the old one matches
neither the index nor an exact directory read; and the commit is made from the
index. `update-index --force-remove` is the one git verb that removes an entry
without asking the filesystem whether the file is still there, and it is a
no-op on a path the index does not hold, so a log that was never committed
renames just as quietly (measured: exit 0, nothing staged).
THE INDEX COMMIT IS FAIL-CLOSED. Committing the index rather than a pathspec
gives up the guarantee `--only` was there for — that nothing else in a shared
checkout rides along — so it is asserted instead: anything staged that is not
one of this write's own pathspecs REFUSES (6) and names it, rather than being
committed under this lane's subject. It applies only to the write that carries
a rename; every other write keeps the path-limited commit byte for byte.
AND THE ASSERTION THAT WOULD HAVE CAUGHT THIS ON ANY FILESYSTEM. The two lines
that watch the rename read `git show --name-status` for the old path and for
the new one. BOTH of them pass on a commit that modified the old and added the
new — which is exactly the pathology — so they were green on macOS while the
tree held two logs. What names it is the tree itself: `git ls-tree -r HEAD --
lanes/log` must hold ONE path for the lane's log, counted case-insensitively so
that a second spelling is a second path. Asserted now for `repoCase-1` and for
`repoIgn-1`, on every platform, red at `928908a` and green here. No assertion
was weakened or deleted; two were added.
ALSO, FROM COPILOT'S SECOND REVIEW: the refusal added in `e35f2a8` for a
published log this checkout has not pulled printed `$lane-s object log` and
`this write-s own commit`, so a person read `repoCase-1-s`. Corrected to
`${lane}'s` and `write's`. This is a `die` string and every one of the
seventy-odd in this file spells a possessive `'s`; the `-s` in COMMENTS is this
file's own convention, eleven of them in `main` before this branch, and is left
alone.
MEASURED, on Linux (a case-SENSITIVE filesystem, so the two-directories case
runs rather than skipping). `bash tests/test_lane_helpers.sh` at `928908a`:
1616 passed, 0 failed, 0 skipped, 0 pending. On this tree: 1618 passed, 0
failed, 0 skipped, 0 pending — the two new tree-count assertions.
`python3 -m pytest tests/test_repo_hygiene.py -q`: 123 passed. `bash -n` on
every file touched.
Lane: openrepotools-3 (openRepoTools-3)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved lane-resolution, log-path, claim-filtering, and argument-handling issues can cause incorrect or non-actionable behavior.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (9)
Previously missed (3) — in code that hasn't changed since the last review.
lane-end:510
- The same stale-checkout case is missed here: if origin has the two case-variant rows but this checkout has only one, status 2 from
canon-laneis ignored, the local scan passes, and the fallbackregister-rowread at lines 814-824 suppresses the useful diagnostics and turns it into a generic exit-1 path.lane-endshould preserve or relay the Amendment 15(d) refusal instead of proceeding without an actionable duplicate report.
lane-start:740 - Once this helper is present, status 2 specifically means that the published register has a case-duplicate pair, not merely that an old helper lacks
canon-lane. On a stale checkout whose local file has only one copy, continuing here leads to the later stderr-suppressedregister-rowcall and a generic exit-1 failure, so the user never gets the two spellings or the Amendment 15(d) refusal. Preserve or relay the current resolver refusal while retaining the old-helper fallback.
restart:373 - The current
canon-lanereturns 2 for the Amendment 15(d) duplicate-row refusal, but this code discards its diagnostics and keeps the typed spelling.register-rowis then also run with stderr suppressed and its 2 becomes the generic failure at lines 384-386, sorestartnever names both rows or returns the actionable refusal for a published case-duplicate pair. Distinguish the current helper's refusal from an unknown old helper, or relay theregister-rowrefusal.
lanes-edit.sh:5184
- This new case-insensitive exclusion has the same regex issue: allowed lane punctuation such as
.is interpreted bygreprather than matched literally. During the post-rebase rescan, a rival can therefore be removed as if it were$CLAIM_LANE/$CLAIM_SKIP, causing the winner check to miss a real competing claim. Use a field-based literal, case-insensitive comparison for both exclusions.
| grep -iv "^$CLAIM_LANE$US" | { [ -n "$CLAIM_SKIP" ] && grep -iv "^$CLAIM_SKIP$US" || cat; } || :)"
lanes-edit.sh:1409
- The
awkexits after the first case-insensitive match, so a published tree containing bothlanes/log/foo.mdandlanes/log/Foo.mdis treated as if it had one log.lane_log_events/lane_log_existsthen read only that path, allowinglanes --lane,last-session, and home/profile resolution to silently ignore part of the append-only history;ensure_logcatches this only when a write occurs. Count multiple published matches and fail closed (or combine them consistently) before returning a path.
| awk -v want="$lpc_exact" 'BEGIN { w = tolower(want) } tolower($0) == w { print; exit }')"
lanes-edit.sh:1826
- The working-tree branch repeats the same silent choice with
head -n1: two local case-variant log files make read-only callers consume one arbitrary file. That can make resume/end/listing decisions from incomplete history even though the writer treats the state as ambiguous; detect multiple matches and fail closed instead of selecting the first.
ll_f="$(log_files_named_ci "$1" | head -n1)"
[ -n "$ll_f" ] && [ -f "$ll_f" ] || return 0
lanes-edit.sh:5420
add-rowchecks onlyrows_named_ci_local, so a stale checkout whoseorigin/mainalready hasrepoCase-1still appendsrepocase-1.commit_pushthen rebases and can publish the second case-variant row—the exact duplicate this amendment saysadd-rowmust refuse. Include the published-register matches in this preflight (while retaining the local scan for unpushed rows) before appending.
ar_hits="$(rows_named_ci_local "$lane_new")"
ar_n="$(printf '%s' "$ar_hits" | grep -c . || :)"
if [ "$ar_n" -gt 0 ]; then
lanes-edit.sh:1441
- The published-path guard only runs when
el_nis zero. If a previous case-only rename left the canonical file in this working tree whileoriginstill has the old-cased path (for example, the commit failed after the filesystem rename), this branch is skipped; the next write stages only the canonical path and leaves the old path in the remote tree, recreating two logs for one lane. Check the published case-insensitive matches even when a local canonical file exists, and reconcile or refuse before writing.
if [ "$el_n" = 0 ] && have_remote_ref; then
el_pub="$(log_path_ci "$lane")"
if [ "$el_pub" != "$(log_path_for "$lane")" ]; then
die "lane ${lane}'s object log is published as $el_pub and this checkout does not have it: the row spells the lane $lane, so the log is $(log_path_for "$lane") (Amendment 15), and writing one here now would leave TWO files for one lane. Pull first — \`git -C $LANES_REPO pull --rebase\` — and re-run; the rename then happens once, inside this write's own commit. Nothing was written." 2
lanes-edit.sh:5184
- The new case-insensitive holder filtering still leaves
first_landed_of's path-to-holder join byte-for-byte (lanes-edit.sh:5160-5161). For a log whose claim line sayslane repohold-2while its canonical file isrepoHold-2.md, that join misses the winner; when two rivals are present the rescan can fall back to the sorted first holder and writeCLAIM-LOSTnaming the wrong lane. Normalize both sides of that join (and add a mixed-case multi-rival race case) before relying on the new case-insensitive state.
# `-i` ON BOTH EXCLUSIONS (Amendment 15), for the reason `claim`-s own
# pre-check carries it: these two remove THIS lane and the lane it is taking
# over from the set of rivals, and the row each removes carries whatever
# spelling the winning line used. Byte for byte, a lane whose earlier CLAIMED
# says `repohold-1` under the row `repoHold-1` loses the race to ITSELF and
# writes a CLAIM-LOST naming itself as the winner — measured, exit 7.
crh="$(state_events | lane_states_on "$CLAIM_OBJ" | holders_of "$CLAIM_OBJ" \
| grep -iv "^$CLAIM_LANE$US" | { [ -n "$CLAIM_SKIP" ] && grep -iv "^$CLAIM_SKIP$US" || cat; } || :)"
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Review effort level: Lite
| for lns_a in ${lns_args[@]+"${lns_args[@]}"}; do | ||
| case "$lns_a" in --all) lns_all=1 ;; esac | ||
| done | ||
| lns_new=(); lns_take=0 | ||
| for lns_a in ${lns_args[@]+"${lns_args[@]}"}; do | ||
| if [ "$lns_take" = 1 ]; then | ||
| lns_take=0 | ||
| if [ "$lns_all" = 0 ]; then | ||
| lns_one="$(canon_lane "$lns_a")" || exit 2 | ||
| else | ||
| lns_one="$lns_a" | ||
| fi | ||
| lns_new+=("$lns_one") | ||
| continue | ||
| fi | ||
| case "$lns_a" in --lane) lns_take=1 ;; esac | ||
| lns_new+=("$lns_a") | ||
| done | ||
| lns_args=(${lns_new[@]+"${lns_new[@]}"}) | ||
| lns_out="$(lanes_rows ${lns_args[@]+"${lns_args[@]}"})"; lns_rc=$? |
… things are refused rather than chosen between — literal case-insensitive row filters, `add-row` asking the PUBLISHED register, two logs for one lane a refusal in every reader, `ensure_log`'s guard on the PATHS not on a count, and `canon-lane`'s 2 relayed by all three commands
WIP, COMMITTED AS FOUND AND NOT YET MEASURED. This is the working tree the
predecessor left dirty when its process died, taken verbatim so that nothing of
it is lost before the round is finished. Six files, +521/-60. It has NOT been
run: no `bash tests/test_lane_helpers.sh` and no `pytest` was measured against
it, and the 296 lines it adds to the suite are claims until one is.
WHAT IS HERE, against Copilot's fourth review of openRepoTools#41 (six inline
comments and nine suppressed, three of the nine "previously missed"):
* `lanes-edit.sh` `rows_named` / `rows_not_named` (:1992) — a lane name is not
a regular expression. `check_lane_name` admits `.`, so `grep -iv
"^$CLAIM_LANE$US"` excluding `repo.x-1` also excludes the real rival
`repoXx-1`. Both `claim_rescan_hook` exclusions, `claim`'s own pre-check
exclusion and `first_landed_of`'s file-name-to-holder-row join go through
them: `awk` on FIELD ONE, `tolower` on both sides, no pattern anywhere.
* `log_path_ci` (:1424) counts instead of taking the first match: two
published paths differing only by case RETURN 2 naming both, and the
no-remote fallback now answers out of the same case-insensitive local scan
every other reader uses rather than naming a canonical path that need not
exist. `lane_log_events`, `lane_log_exists` and `lane_objects` carry that 2
rather than flattening it into "no log".
* `ensure_log` (:1453) tests the PATHS and not `el_n = 0`, so the state a
case-only rename leaves when its commit does not land — canonical file here,
old-cased path still published — is refused rather than walked past.
* `write_event` (:3281) exempts the path the log is ACTUALLY at from
`refuse_dirty_checkout`, which need not be the canonical one until this
write renames it.
* `add-row` (:5532) asks the published rows as well as the local ones, with
`log_sync` in front of them, and says which of the two scans found the row.
* `lane-end` (:513), `lane-start` (:741) and `restart` (:374) relay
`canon-lane`'s 15(d) refusal by its own words — no helper predating this
amendment emits them from an unknown subcommand — and exit 2, instead of
letting a later stderr-suppressed `register-row` turn it into a generic 1.
* `tests/test_lane_helpers.sh` (+296) — a `Copilot round 4: names and paths`
section with a bare second clone as the peer, `hash-object` /
`update-index --cacheinfo` / `commit-tree` plumbing so that TWO paths
differing only by case can be published where no case-insensitive working
tree could hold them, and cases for two published logs, the published pair
against a checkout holding one row, `add-row` against a stale checkout, a
rename whose commit never landed, a log on no origin, a lane name that is a
pattern, and a two-rival race the byte-for-byte join named wrongly.
* `docs/README-lanes.md` (+8) — the relayed 2, `add-row`'s published scan and
the log held to the same rule.
WHAT IS NOT DONE: the suite has never been run against any of it; four of the
round's findings are not addressed here at all (`ls --` under BSD `ls`, the
`--dir` run still refused for a two-directory ambiguity the flag settles, the
`lanes --dir` array expansions, and the self-holder read's grouping); and no
finding of the round has been judged NOT real yet.
Lane: openrepotools-3 (openRepoTools-3)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…NAME and never as a pattern, two published logs for one lane are a refusal in every reader rather than whichever `ls-tree` happens to name first, `add-row` asks the register a peer has already pushed, `ensure_log` tests the PATHS and not a count, and `canon-lane`'s 15(d) refusal is relayed by all three commands — six taken, four declined with what was measured
THE COMMIT BELOW THIS ONE IS THE PREDECESSOR'S WORKING TREE, COMMITTED AS FOUND
AND NEVER RUN. This commit is the first measurement of it, the four verdicts the
round still needed, and the one decline it left argued rather than asserted.
WHICH COMMENTS ARE ACTUALLY ROUND 4'S. GitHub rewrites an inline comment's
`commit_id` to the head of the branch, so all EIGHT open inline threads read as
`617d383`'s; the review ids do not. Round 4 is review `5199971901` — three
inline comments (`lanes-edit.sh:1412`, `:5571`, `:6175`) and nine suppressed
ones, three of those marked *previously missed*. Of the other five threads, two
are round 1's (`5198408024`, on `48f2111`) and were answered in `e35f2a8`, two
are round 3's (`5199527132`, on `928908a`) — one answered here, one declined
below — and one is round 2's (`5199048577`), answered here.
SIX TAKEN, EACH WITH THE ASSERTION THAT HOLDS IT. The code is the WIP commit's;
what follows is what it was measured against, all of it in the new
`Copilot round 4: names and paths` block at the end of the suite's
`== Amendment 15: a lane name is one name under any case ==` section.
* A LANE NAME IS NOT A REGULAR EXPRESSION (`lanes-edit.sh:2005` `rows_named` /
`rows_not_named`, used at `:5300`, `:5304`, `:5274` and `:5708`).
`check_lane_name` admits `.`, and `grep -iv "^$CLAIM_LANE$US"` excluding
`repo.x-1` also excludes the real rival `repoXx-1`. Asserted with exactly
that pair: the claim exits 2 naming `repoXx-1` as the holder and writes
nothing to `repo.x-1`'s log.
* AND THE RACE'S WINNER IS JOINED ON A FILE NAME (`first_landed_of:5274`).
The file carries the row's spelling from the first write; the holder row
carries whatever the winning LINE was typed with. `repoRace-2` lands first
with its `CLAIMED` spelled `reporace-2` and `repoRace-3` second spelled the
row's way: the claim exits 7 naming `@repoRace-2` and never `@repoRace-3`,
which is the one `LC_ALL=C sort` puts first and the one the byte-for-byte
join fell back to.
* TWO PUBLISHED LOGS FOR ONE LANE ARE A REFUSAL (`log_path_ci:1443`, carried
by `lane_log_events:1889`, `lane_log_exists:1899` and `lane_objects:2026`).
`lane-objects repotwo-1` on a tree holding `lanes/log/repoTwo-1.md` and
`lanes/log/repotwo-1.md` exits 2 naming both and citing 15(d), and the write
behind it appends nothing.
* THE NO-REMOTE FALLBACK IS THE LOCAL SCAN (`log_path_ci:1450`), because
`claim` computes its dirty-checkout exemption from it: a lane whose log is
still spelled the old way and has never been pushed claims 0 instead of
being refused for its own uncommitted file.
* `ensure_log` TESTS THE PATHS (`:1499`). A canonical file here beside an
old-cased published path — a rename whose commit did not land — is refused
naming which half landed, where the `el_n = 0` gate walked past it.
* `add-row` ASKS THE PUBLISHED ROWS (`:5546`). A row `origin/main` carries as
`repoPush-1` refuses a local `repopush-1` naming the row and the `pull
--rebase`, and a row only this checkout has still refuses as it did.
* `canon-lane`'s 2 IS RELAYED (`lane-end:513`, `lane-start:741`,
`restart:374`). With the pair published and one row here, all three now exit
2 naming both spellings and citing 15(d) rather than spending a generic 1
out of a stderr-suppressed `register-row`; and a helper that predates
`canon-lane` — a two-line fake in the sandbox — still falls through to
`lane-end`'s own row scan, which is asserted in the same block.
THE PEER THAT PUBLISHES WHAT THIS CHECKOUT NEVER PULLS. Five of the six can only
be seen from a checkout that is BEHIND, and one of them needs two paths
differing only by case in ONE published tree — which no working tree on a
case-insensitive filesystem can hold, and macOS is the platform this matters
most on. So the sandbox's peer writes with `hash-object`, `update-index
--cacheinfo`, `write-tree` and `commit-tree` and never checks anything out, and
takes the pair off `main` again before any later `pull --rebase` in the suite
would have to materialise it.
FOUR DECLINED, WITH WHAT WAS MEASURED.
* *"BSD `ls` rejects `--`, and the suppressed error makes every
case-insensitive log scan empty"* (round 3, `lanes-edit.sh:1397`, now
`:1410`). It does not: BSD `ls` ends its options at `--` through
`getopt(3)` like every other POSIX utility, and `tests-macos` has been
GREEN throughout on the two assertions that can only pass if both `ls --`
scans answer there — the rename recorded inside the write's own commit
(`git show --name-status` naming `lanes/log/repocase-1.md` AND
`lanes/log/repoCase-1.md`), and `lane-start`'s own step line
`repo repocase → repoCase` out of the twin
scan at `lane-start:677`. Three assertions are added so the decline is
measured rather than argued, on every platform: `ls --` exits 0, lists
exactly what the bare form lists, and lists the log a lane's own scan has to
find in it. Dropping `--` would hand a directory whose name begins with `-`
back to `ls` as flags and change nothing else.
* *"All four array expansions in this block are unquoted … a valid `lanes
--dir '/tmp/project with spaces'` splits the path"* (round 4,
`lanes-edit.sh:6175`). `${lns_args[@]+"${lns_args[@]}"}` is the bash 3.2
idiom for an array that may be EMPTY under `set -u` — the one the
`lanes_rows` call has always used — and the quotes inside the `+word` half
are honoured, so the elements survive whole. Asserted rather than argued in
the same block: `lanes --dir "$HOME/projects/a lane dir with spaces"` is not
a usage error, no word of the path arrives as an argument of its own, and
the run is simply a directory no lane is in (8). A split would have reached
`lanes_rows` as three arguments and come back 64.
* *"Canonicalizing `lane` here does not make the self-holder check
case-insensitive … `lane_states_on` still groups rows by raw `$2` in
`PER_LANE_AWK`"* (round 1, `lanes-edit.sh:5562`). Both halves landed in
`e35f2a8`: `PER_LANE_AWK` keys on `tolower($2)` (`:1967`) and the exclusion
has been case-insensitive since. It is asserted directly — `repoHold-1`,
whose own `CLAIMED` says `repohold-1`, claims its own object 0 and the
output carries neither the pre-check's *"Rule 1: the lane stops and
reports"* nor the rescan's *"CLAIM LOST"*. What was left of the finding is
the REGEX, which is the first item taken above.
* *"When `--dir` is supplied, this block still scans `$PROJECTS_ROOT` and
refuses if two case variants exist"* (round 1, `lane-start:678`). Answered
in `e35f2a8`: the refusal carries `[ -z "$dir_override" ]` (`lane-start:695`)
and a `--dir` run gets a note saying which ambiguity is left instead.
Asserted where the pair can exist at all — `lane-start --dir
$HOME/projects/repoTwin repotwin 1` exits 0 and takes the directory it was
given, inside the case-sensitive-filesystem branch.
EVIDENCE, on this workstation (Linux, a case-SENSITIVE filesystem, so the
two-directories case runs rather than skipping).
`bash tests/test_lane_helpers.sh` on the WIP commit's tree — the predecessor's
work, measured for the first time: **1675 passed, 0 failed, 0 skipped, 0
pending**, against 1618 at `617d383`, fifty-seven assertions added and none
removed. On THIS tree: **1678 passed, 0 failed, 0 skipped, 0 pending**, the
three `ls --` assertions above and nothing else moved.
`python3 -m pytest tests -q`: **549 passed** in 1207 s, on the exact tree this
commit records, which runs that suite a third time through
`tests/test_lane_helpers_suite.py` and asserts its footer says `0 failed`.
`python3 -m pytest tests/test_repo_hygiene.py -q`: 123 passed, on every edit as
it was made. `bash -n` on every file touched. No assertion was weakened or
deleted anywhere in the round.
CI at `617d383`: `tests` pass (9m21s), `tests-macos` pass (23m35s),
`tests-no-submodule` pass (9m30s), `tests-windows` pass, SonarCloud pass.
Lane: openrepotools-3 (openRepoTools-3)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate and critical findings remain in lane-start and lanes-edit.sh, including checkout selection, register synchronization, duplicate-log handling, swap resolution, and dirty-path capture.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
lane-start:703
- When
--diris supplied, that option has already selected the checkout, but this branch still rewritesrepofrom a matching child of$PROJECTS_ROOT. For example, with--dir /tmp/otherCheckout repocase 1and$PROJECTS_ROOT/repoCasepresent, the new lane becomesrepoCase-1while its recorded directory isotherCheckout. That makes the lane's<repo>half disagree with the selected checkout and contradicts the documented real-directory rule; skip this rewrite when--diris set or derive the name from the selected directory.
if [ "$repo_hit_n" = 1 ] && [ "$repo_hits" != "$repo" ]; then
step "repo $repo → $repo_hits (Amendment 15(a): the checkout's real directory name under $PROJECTS_ROOT)"
repo="$repo_hits"
lanes-edit.sh:5484
- This writer now calls
canon_lane, which readsorigin/<branch>, but it never runslog_syncfirst. A peer can add or merge the referenced row after the last fetch;commit_pushrebases only after this attribution decision, so the Rule 6 append can land with a stale orunknownlane in its commit subject. Fetch before resolving the candidate, as the other writer arms do.
[ -z "$lane_tag" ] || lane_tag="$(canon_lane "$lane_tag")" || exit 2
lanes-edit.sh:4815
nm[l]preserves the spelling from thePAUSEDline, butswapped_laneslater uses that value as an exact Git path inlog_path_for. On a case-sensitive checkout, a swap line such asrepopair-1whose single file isrepoPair-1.mdmakes the pickaxe return no commit, so the candidate gets the fallback rank/UTC ordering andrestartcan choose the wrong lane. Resolve the log path case-insensitively (or canonicalize it) before thegit loglookup.
{ k = tolower($2); p = pos($10, $11)
if (!(k in mp) || p >= mp[k]) {
mp[k] = p; verb[k] = $3; utc[k] = $1; pay[k] = $8; uuid[k] = $4; ws[k] = $5; obj[k] = $6; nm[k] = $2 } }
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Review effort level: Lite
| function pos(pf, pn) { return pf "\034" sprintf("%09d", pn) } | ||
| BEGIN { FS = sep } | ||
| $6 == o { p = pos($10, $11); if (!($2 in u) || p >= u[$2]) { u[$2] = p; L[$2] = $0 } } | ||
| $6 == o { k = tolower($2); p = pos($10, $11); if (!(k in u) || p >= u[k]) { u[k] = p; L[k] = $0 } } |
| ensure_log "$we_lane" | ||
| [ -n "$LOG_RENAMED_FROM" ] && we_paths+=("$LOG_RENAMED_FROM") |
| lane_tag="$(row_lane_ci "$cand")" | ||
| fi | ||
| canon_lane "$cand" >/dev/null || exit 2 | ||
| lane_tag="$(row_lane_ci "$cand")" |
…dment 12 section and every `lane-start` window binding with it is fixed, and the artifact count, the docs and the pytest half of act 5 are written — one shell run behind them, the next not yet made WIP BECAUSE NO SUITE HAS RUN SINCE THE FIX. The run that FOUND it is measured below; the run that proves it is not yet made, and neither is any `pytest`. THE DEFECT, and it was one line. `transcript_holders` never initialised `th_here_prof`, and `lanes-edit.sh` runs under `set -u` (line 292): pass 2's `[ -z "$th_here_prof" ]` is reached for the FIRST record that is not this window's, and an unset variable there exits the shell with `unbound variable`. Measured on the predecessor's state at `3befe7c`, `bash tests/test_lane_helpers.sh`: **1468 passed, 124 failed** — 73 of the failures the WHOLE of the new `== Amendment 12 ==` section, each one stderr reading `lanes-edit.sh: line 4961: th_here_prof: unbound variable`, and the rest every `lane-start` case that reaches step 3b's window binding, because `dup_check` reads that 1 as "the records could not be read" and refuses fail-closed (`the same lane live in THIS window is not a collision`, `lane-start does NOT refuse the window its own session is running in`, all four `veto` cases, …). `th_here`, `th_prof`, `th_wrc` and `th_final` are initialised beside it. THE COUNT, EVERYWHERE IT IS DERIVED OR STATED (act 3's second half): 18 -> 19, because `--install` now merges TWO entries into one `settings.json`. `tests/test_openrepotools_command.py` derives it — `ARTIFACTS = len(INSTALLED) + 2*len(SKILL_NAMES) + 2*len(COMMAND_NAMES) + HOOK_ENTRIES`, the `+ 1` that was hand-kept now a named `HOOK_ENTRIES = 2` — and `openRepoTools` states it in six places, README.md in two, `docs/README-lanes.md` in one. "THE SEVEN ARTIFACTS THAT ARE NOT ONE OF THE ELEVEN FILES" is eight. DOCS (act 6): `docs/README-lanes.md` gains "The name guard and the lock (Amendment 12)" before "Hand edits" — the triple, the (b) table filled in, the lock and its pane conditions, the offer and its three answers, Amendment 18(h)'s duplicate read and its three callers, clause (f)'s line, fail-closed with the one bypass, and the scope. README.md's install paragraph names the second entry, what it does, and that a person's own `UserPromptSubmit` hooks are left where they are; its cap moves 409 -> 417 with a dated entry that says what the eight lines bought. PYTEST (act 5's other half): seven cases in `tests/test_install_skill_and_hook.py` — the entry byte-identical and with NO `matcher`, the `|| true` that must NOT be on it asserted on its own, twice adds one entry, a differing `guard` entry refuses and places nothing, `~/projects/xFactory/guard.sh` is NOT a conflict (the verb collides, not the file — R-A9-14 one event over, and this estate's usage guard is exactly that string), the pair is ONE all-or-nothing merge, and one write places both. ALSO: the comment that resolved `lane-start` "the way `restart` finds `lanes-edit.sh`" no longer names a file Amendment 18 Addendum 2 is removing (#43); it names this file's siblings. WHAT IS LEFT: the shell suite re-run and a `pytest`; hardening the two hooks against a fatal inside the read (this crash exited `session-start` 1, against R-A8-1's "always exits 0", and would exit `guard` 1, which lets a prompt THROUGH — the opposite of clause (d)); act 2 verified done and to be reported; `git merge origin/main` once #41 lands. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dment 12 branch — four conflicts in `lanes-edit.sh` and one in the suite, every one of them the two rounds writing in the same place about the same rule
MERGE AND NEVER A REBASE: this branch is pushed, and its two WIP commits are
the record of a session that was killed mid-work.
THE FIVE CONFLICTS, and each resolution keeps BOTH rounds rather than picking:
1. `lanes-edit.sh`'s EXIT CODE TABLE, row 2. Both rounds added a meaning to
the same number and both wrote "FIVE MEANINGS ON ONE NUMBER" over a list
that had four. The resolution carries both — Amendment 15's two rows
differing only by case, and Amendment 12's BLOCKED PROMPT — and the count
is SIX, with the paragraph below it renumbered from "THE FIFTH" to "THE
SIXTH". A table that says it is complete and is not is worse than no table.
2 & 3. `rows_named_ci`. The two rounds wrote the SAME FUNCTION, body for body —
this branch spelled it that way on purpose, saying so in the comment it
carried ("so the two collapse into one when it lands rather than becoming
two readings of one rule"), and they collapsed. Amendment 15's header stays,
because it is the one that states the RULE and `canon_lane` sits under it;
this round's comment becomes three sentences inside it saying why a third
caller asks the same parse: the guard needs the COUNT and has to print its
triple before it refuses, so it cannot take `canon_lane`'s own `note` + 2.
One parse, three readings, no second rule about what a lane name is.
4. The `*)` arm's subcommand list: both `guard`/`transcript-holders` and
`canon-lane`, which `test_the_unknown_subcommand_refusal_names_every_subcommand_there_is`
derives from the dispatcher's own arms and would have failed on either alone.
5. `tests/test_lane_helpers.sh`: both rounds appended a section immediately
before `== the workstation seam ==`. Both are kept, Amendment 12's first and
Amendment 15's second, which is the order #25's brief asks for and leaves the
seam second-to-last as it has always been.
AND ONE THING THE MERGE MADE NECESSARY THAT NO CONFLICT SHOWED. The Amendment
12 section now sits BETWEEN two others instead of at the end, and it had been
tearing its fixture DOWN — `unset FAKE_TMUX_WINDOWS FAKE_TMUX_WINDOW_NAME`,
`export FAKE_TMUX_WINDOW=testsess:@1` — rather than putting it back. That was
harmless while nothing followed it and is a section changing the premise of
every case after it from a distance now that Amendment 15's does. The three
variables are CAPTURED at the top of the section and restored at the bottom,
each with its own `if [ -n … ]` so that "was unset" comes back unset, which is
the state `main` ran Amendment 15's cases in.
`restart` moved under the merge; it is `main`'s change (#41) and this branch
does not touch that file — Amendment 18 Addendum 2 removes it in #43.
Lane: openrepotools-3 (openRepoTools-3)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ts conflict, and `canon-lane` comes into `lane <name>` with the act it was written for FOUR CONFLICTS, EACH ONE A PLACE THE TWO AMENDMENTS TOUCH THE SAME LINE. 1. `restart` — modify/delete. **THE DELETION WINS** (Amendment 18 Addendum 2, in force 2026-09-14T16:50:32Z verbatim "ratify"). #41 had added a 38-line Amendment 15 block to that file; the block is not lost, it is PORTED into `lane` below, because Addendum 2 moves the act and not only the word. 2. `lanes-edit.sh` — the unknown-subcommand refusal. **UNION**: `lane-groups` and `next-free` (Addendum 1) and `canon-lane` (Amendment 15) are three new reads and all three are named. 3. `docs/README-lanes.md` — the reads table. Amendment 15's sentence about `window-lane` answering in the register row's own spelling is KEPT, with the word it names changed from `restart <lane>` to `lane <name>`. 4. `tests/test_lane_helpers.sh` — both branches added a section immediately before `== the workstation seam ==`. **UNION, Amendment 15 first**: that section keeps the position it landed on `main` in and therefore the state it was verified against, and Addendum 1's follows it, still the last section before the seam. `canon-lane` IS NOW `lane <name>`'s RESOLVER, which is the whole of #41's `restart` block moved with the act: one resolver shared by every command that takes a `<lane>`, because four implementations of *which row is this name's* is how the four come to disagree about it. Its `2` is two answers and they are told apart by the refusal's own words — a helper predating Amendment 15 spends 2 on an unknown subcommand and says nothing about any amendment, and is a silent fall-through; `canon-lane`'s OWN 2 is the register spelling one lane two ways, which no read below can resolve and which only a person's 15(d) merge settles, so it is RELAYED and never reported as a read that failed. The estate-wide read under it stops being the resolver and becomes what it always was underneath: the fallback for a workstation that has not taken the install. FOUR OF #41's CASES MOVE TO THE WORD THAT PERFORMS THE ACT, behaviour unchanged: `lanes --lane repowin-1`'s column 10 is now `lane repoWin-1`; `window-lane`'s comment names the word its three callers hand the spelling to; `repocase-1` is resolved by `lane <name>` rather than by `restart`; and the 15(d) pair is relayed by `lane <name>` with both spellings and the merge named. Three more lines of #41's prose named a command that is no longer on anyone's PATH and now name `lane`: the manual's relay sentence (`docs/README-lanes.md`) and two comments in `lanes-edit.sh` describing what column 10 offers a reader. Baselines measured for this merge, each a full run of `tests/test_lane_helpers.sh` out of a clean extract of that commit: main 92eab6c (this branch's base) 1500 passed, 0 failed, 0 skipped, 0 pending this branch before the merge 1595 passed, 0 failed, 0 skipped, 0 pending The merged run is the next commit's evidence; this one is the resolution. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…refusal and not an attach onto another lane's window, a binding whose liveness could NOT be read is never launched, `--install` REMOVES the `restart` it once placed, and the flags the two aliases were handed are forwarded or refused rather than dropped EIGHT FINDINGS, EIGHT TAKEN, NONE DECLINED. Copilot's first review of #45 (2026-09-14, "🟡 Changes recommended", 3 inline and 5 suppressed). Each one below is a defect THIS pull request introduced, and each is proved by a case that fails against the commit before it. ## The two it called critical **`lane:610` — the select was part of the fence and was written as a courtesy.** All four attach paths put the terminal on the lane's OWN window, and three of them do something after it. The outside-tmux path spelled that select `|| :` and then `exec tmux attach`: a window that closed in the half second between the id-AND-session check and the act left the attach landing on whatever window that session had current — ANOTHER LANE — which is precisely what the fence above it exists to stop. One `select_window_or_refuse` now carries all four sites, naming what had already been done (a client switched, a window moved) so the refusal says where the person is. **`lane:777` — an unread session record was being read as a proof.** `lanes_rows` says on STDERR, and goes on answering, that this workstation's session records could not be read, in which case its STATE column is *"the LOG's verb alone: a lane that is live may show as IDLE or PAUSED here"* (`lanes-edit.sh:4628`). This command relayed that sentence and then grouped the row AVAILABLE and launched it — a second process on a lane that may still be running, which is the collision clause (h) is about and the fall-through Amendment 7(d) forbids. `read_rows` now catches the sentence out of the stderr it is already reading, and `go_available` refuses on it — for a BINDING alone: a PAUSED lane is parked by its own last line, needs no liveness proof, and launches exactly as it did. ## The moderate ones **`openRepoTools:234` — a word that left the list had not left the directory.** Dropping `restart` from `INSTALLABLES` stops THIS run from copying it and does nothing about the copy the last run placed, so every workstation that took the install before Amendment 18 Addendum 2 would have kept `restart` on its PATH for ever — against the addendum's own words, *"`restart` is no longer a command a person is given"*. `--install` now RETIRES it, and removes only what it wrote: the test is the header this installer puts in every command it owns, so a person's own script of that name is NAMED, left exactly where it is, and the `rm` that removes it is printed for them to run. **`lane:710` — the `f` answer dropped `--dir`.** The flag is in this word's own parser and `lane-start` takes the same one, so `lane --dir <path>` followed by `f` opened the new lane in a checkout nobody had named — Evidence 3's silent loss, at the one moment a lane is created. **`lane:701` and `lane-start:658` — PATH before the sibling, against this family's own rule.** `lane` states it fifty lines above its own `$LANES_EDIT` lookup: *"beside this command first … then on PATH. One resolution, so a workstation with two copies never reads one and writes the other."* Both new lookups had it the other way round. `lane-start`'s older `lanes` lookup took the same correction in the same commit, so the file carries ONE order rather than two. **`lane-start:661` — the alias dropped every flag it had been handed.** The parser takes `--dry-run`, `--no-launch`, `--confirm`, `--yes`, `--dir` and `--estate` before the zero-positional branch is reached, and the `exec` carried none of them: `lane-start --no-launch`, a flag whose whole promise is that nothing starts, became an interactive pick that launches. `--dry-run` and `--dir` mean the same thing in `lane` and go with it; the rest are REFUSED BY NAME, with the form that does take them filled in. A flag silently dropped is a promise that was not kept. **`lanes:432` — `|| :` collapsed every status of the `next-free` read.** The footer then told a person to take the install for a read that had FAILED. `2` is the un-upgraded workstation and still says so; anything else names the code it got and offers no position (Amendment 7(d)) — and a `0` that comes back with nothing a position can be made of is in that second class. ## And one this repository's own hygiene caught on top `grep -q "<pattern>" -- <file>` puts the `--` AFTER an operand, which BSD's `getopt` reads as a filename; `test_no_shipped_bash_ends_its_options_after_an_operand` named it in `lane` before any of it ran on macOS. Both new greps take `-e`, like the line above them that always had. ## Evidence python3 -m pytest tests/test_openrepotools_command.py \ tests/test_repo_hygiene.py \ tests/test_install_skill_and_hook.py -q before this commit 197 passed after 199 passed (the two the retirement adds) Shell suite on the commit BELOW this one, which is this branch merged with `origin/main` and before any of the above: 1781 passed, 0 failed, 0 skipped, 0 pending and for comparison, each a full run out of a clean extract of that commit: main 92eab6c (this branch's base) 1500 passed main 8345d2f (after #41) 1678 passed this branch before the merge 1595 passed The run that covers this commit is `python3 -m pytest tests -q`, in flight as this is written; it is committed now at a coordinator's swap notice and the footer goes in the pull request. New cases, all in `tests/test_lane_helpers.sh`'s Amendment 18 Addendum 1 section unless said otherwise: a `select-window` that fails on the outside-tmux path and on the move path (the section's fake tmux grows one knob, and the move is still in its log because it happened); a binding refused where the liveness read did not answer, with a PAUSED lane beside it still launching; `f` carrying its `--dir`, resolved; bare `lane-start` refusing `--no-launch`, naming every dropped flag rather than the first, and forwarding `--dry-run` through to the act `lane` then prints instead of performing; `lanes` listing where the `next-free` read FAILED without calling it an un-upgraded workstation. In `tests/test_openrepotools_command.py`: `--install` removing a pre-seeded `restart` that carries this installer's header, and leaving one that does not while printing the `rm`. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e `lane-start` may not — its veto 1 never moves a uuid another row records, and without that last write the guard refuses for ever on a state it created by obeying the person
THE DEFECT, MEASURED, AND IT IS A LOOP. Clause (h) rule 2 names the end state
and rule 3 repeats it: after a `yes`, *"window X, session X, ROW X STAMPED WITH
THIS UUID"*. `lane-start --no-launch` does every other part of it and CANNOT do
this one — its step 3b VETO 1 (Amendment 11 clause (d) rule 1) never takes a
uuid that belongs to ANOTHER ROW, and after the rename this uuid still belongs
to the lane being left. So it mints a fresh id instead. In the suite: `yes`
moved the window to `repoGD-2`, whose row then read `STARTED by 7a01ae69…`
while the session was `aaaa0012-1111…`. The NEXT prompt therefore finds a lane
window whose row does not name this transcript — the last row of the (b) table
— and refuses with `lane-start --no-launch repoGD 2`, which vetoes for the same
reason and changes nothing. A blocking hook that refuses for ever, on a state
it created by obeying the person, is the worst outcome this surface has.
THE VETO IS RIGHT AND STAYS. It exists for the take nobody asked for —
Evidence 2(b), *"`lane-start openXfactory-5` typed from a window named
`openRepoProject-1`"* — and what clause (h) rule 4 forbids is moving a uuid
between rows WITHOUT the person's `yes`. Here there is one, on the record,
answered at this very prompt. So `guard_bind_uuid` makes that one write after
`lane-start` returns, with `lane-start`'s OWN anchor discipline unvaried: the
anchor is the PUBLISHED last id, so a checkout whose copy of the row is older
than the one that landed refuses rather than writing a cell that no longer
matches; a cell recording no uuid at all is anchored on `none recorded`, the one
other text this act can meet; anything else prints the act and guesses at
nothing. Both cached copies of the register are dropped first — the variable and
the file every subshell reads — because this is the one read in the file that
happens AFTER a writer has moved the ref, and a row `lane-start` has just
CREATED is not in the cache at all.
AND TWO FIXTURES IN THE NEW SECTION THAT DESCRIBED A WORLD THAT NO LONGER
EXISTS, both of which made real assertions pass over the behaviour they name:
* `$G_OUT` IS KILLED hundreds of cases above this section (the liveness
section's own teardown), so every Amendment 18(h) case here was writing a
record for a DEAD pid — which `record_is_live` correctly rejects, so the
guard exited 0, `lane-start` bound the transcript, and `lane-end --retire`
answered 8. Twelve of the twenty failures were that one line. The section
starts a live process of its OWN and kills it at its end, and the suite's
`cleanup` trap takes it too.
* `add-row` HAS REFUSED TWO ROWS FOR ONE NAME since Amendment 15 landed
(#41), which is the point of the guard's own row for that state: the pair
can no longer be created by the tooling, only met. So it is seeded the way
it actually arrives — straight into the register and pushed — the same
`add_seed_row` + commit the Amendment 15 section uses.
TWO REGRESSION TESTS FOR THE TWO CONTRACTS THE `set -u` CRASH BROKE, each
holding the pair of hooks to its OPPOSITE rule against ONE fixture: the same
unreadable session records refuse the prompt (clause (d)) and still leave
`session-start` exiting 0 with its whole block printed, down to the `(no fetch)`
tail every branch ends with (R-A8-1); and a register this workstation cannot
read blocks the prompt with a 2, never the dispatcher's 1 — the exception
AGENTS.md now carries, and the one number that blocks a prompt at all.
Docs: the manual's offer section states the last write and why `lane-start` may
not make it.
Lane: openrepotools-3 (openRepoTools-3)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d as 8345d2f while this branch was in its #44 round-1 fix, and the two touch disjoint files — `canon_lane` and Amendment 15 tooling in `docs/README-lanes.md`, `lane-end`, `lane-start`, `lanes-edit.sh`, `restart` and `tests/test_lane_helpers.sh`; this branch's round 1 in `openRepoTools`, `README.md` and three of the `tests/` files — so this is a clean merge with nothing to reconcile. `python3 -m pytest tests -q` on this branch alone, just before the merge: 602 passed in 1124.86s (0:18:44), 0 failed. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… follows a SYMLINK a person made, the select's target names its session, a moved window's refusal names where it IS and not where it came from, and macOS's `/var` is why `tests-macos` was the only red FOUR FINDINGS ON MY OWN FIX ROUND, AND ONE ON THE PREDECESSOR'S, and the first of them could have deleted something somebody owns. ## Copilot round 2 (on `eb10256`), four findings, four taken **`openRepoTools:262` — `-f` FOLLOWS THE LINK.** The retirement's ownership test was `[ -f "$target" ] && grep -qF …`: a person's own `restart` SYMLINK pointing at a file that happens to carry this installer's header would have been UNLINKED by it. `plan_install_targets` refuses a `-L` target for exactly that reason — `unplaceable_kind` names the link rather than writing through it — and the retirement now holds to the same rule. `[ ! -L "$target" ]` first, so a link is NAMED, left, and the `rm` printed; both it and what it points at are still there afterwards, which is the new case. **`lane:666` — the select's target was an `@id` alone.** tmux resolves a bare `@id` by picking the "best" session holding that window, and PICKING is the one thing this command's id-AND-session fence exists to stop. Every select is `<session>:<@id>` now — in the acts and in the `--dry-run` lines that print them — so the act is the one the record described or it is nothing. **`lane:706` — after the move, the refusal named the SOURCE.** `move-window -a -s <src>:<idx>` puts the window in THIS session and a detached source with no windows left closes itself; the refusal under it still said "in session <src>" and offered `tmux attach -t <src>`, which is a session that may be gone. `select_window_or_refuse` takes the session it means, and after a move that is `$gl_this`. **`lane:589` — the no-directory recovery dropped the profile that was typed.** `lane <name> <profile>` on a lane whose record has a profile but no directory passes the profile check and lands on the directory refusal, whose one-run exit was `lane --dir <path> <name>` — following which walks straight back into the NO-PROFILE refusal. It carries `${profile:+ $profile}` now. ## And the one CI found that no review did `tests-macos` on `fce18d5` was the only red job: **1764 passed, 2 failed**, both in the predecessor's `--dir` case — FAIL …launching it under the profile that was typed, with the directory named FAIL …from that directory macOS's `$TMPDIR` is under `/var`, a symlink to `/private/var`. `lane` resolves `--dir` ONCE, where the operator typed it, because it `cd`s and then hands the same string to a launcher that would resolve it a second time from somewhere else — so the launcher sees `/private/var/...` while the case asserted the sandbox's own `/var/...`. Asserting the unresolved spelling is asserting that the resolution did NOT happen, which is the one thing that flag must do. The suite already had `$A11_DIR_R` for the relative-`--dir` case fifty lines down; it is computed ONCE now, beside `$A11_DIR` itself and AFTER the `mkdir` that creates it — a `cd` into a directory that does not exist yet falls back to the unresolved spelling and is the same defect written twice. The other three CI jobs on that commit were green: `tests`, `tests-no-submodule` and `tests-windows`. ## Evidence python3 -m pytest tests -q on the round-1 tree: 573 passed in 1406s (the shell suite inside it: 1781 passed, 0 failed, 0 skipped, 0 pending) and for the record, each a full run out of a clean extract of that commit: main 92eab6c (this branch's base) 1500 passed main 8345d2f (after #41) 1678 passed this branch before the merge 1595 passed New cases: the select assertions take the session-qualified target; the move's refusal is asserted to name the session the window is in NOW and never to send a person back to the source; and `--install` is run against a SYMLINK of a retired name, which must survive with what it points at. The run covering this commit is `python3 -m pytest tests -q`, in flight as this is written. A NOTE ON HOW THESE RUNS WERE SEQUENCED. The concurrency guard this work was briefed with — `while pgrep -af 'python3 -m pytest' | grep -v pgrep >/dev/null` — is a NO-OP in this harness: `grep` is a shell function from the session's zsh snapshot and returns 1 with stdout on `/dev/null` even when it matched, so the loop never waits. Measured: the same pipeline captured into a variable gives rc=0 and five matching lines. Four agents were running `pytest` at once past that guard. The form that works splits the pattern so it cannot match the guard's own command line: pat='python3 -m pyt'"est"; while [ "$(pgrep -f "$pat" | wc -l)" -gt 0 ]; do sleep 20; done Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…of had two, the `f` answer no longer vanishes from the question without a word, and a position printed by a read that then FAILED is not a position
FOUR FINDINGS, FOUR TAKEN, and three of them are the SAME defect the round
before fixed one file over: a read whose status was thrown away with `|| :` and
whose emptiness was then read as an answer. Amendment 7(d) is the rule and it
is one rule; taking it in `lanes` and leaving it in `lane` was taking half.
**`lane:960` — the `f` answer vanished from the question with nothing on
screen.** The picker's `next-free` read spent `|| :` on every status, so on an
un-upgraded workstation — or a read that broke — the listing simply lost its
new-lane action and the question narrowed to `[1-N, q]` with no explanation at
all. It now says which: `2` is a `lanes-edit.sh` predating Addendum 1 and names
the install; anything else names the code it got and the read to run by hand.
Neither is a position of 1.
**`lane:751` — `f`'s own read discarded its status too, and that one ALLOCATES.**
A `next-free` that printed digits and then failed handed those digits straight
past the numeric fence below it and into `lane-start`, which is a NEW LANE
opened at a position this estate may well have taken. The status is read first
now and any non-zero is a refusal naming it.
**`lane:322` — THREE STATES, because "I could not read whether the proof was
made" is not "the proof was made".** With no writable `$TMPDIR` the helper's
notes go straight to the terminal and this command cannot read them back, and
`LIVENESS_UNREAD` stayed `0` — so a binding whose liveness may never have been
established was launched anyway, which is the collision clause (h) exists for
and exactly the fall-through Amendment 7(d) forbids. `0` the proof was made ·
`1` the helper SAID it could not make it · `2` this command could not read
whether it did; `go_available` refuses on `1` and `2` alike, in each one's own
words, and a PAUSED lane — parked by its own last line — still launches under
every one of them.
**`tests/test_repo_hygiene.py:34` — the inventory's own description said
"four".** `SHIPPED_BASH` has held six since this branch added `lane` beside
`lanes`, and the paragraph above it still told a reader the macOS job parses
four. A description that contradicts the list it explains is the drift these
comments exist to prevent.
New cases, all in the Amendment 18 Addendum 1 section: the pick listing where
`next-free` is MISSING and where it FAILED, each saying why `f` is not offered
and never offering a position; `f` answered on a failed read, refusing with 1
and starting nothing; and a binding refused under a `$TMPDIR` that cannot hold
a temporary file, with a PAUSED lane launching beside it. The two `next-free`
wrappers move up to the fixtures so the pick's cases and the listing's cases
read the same two fakes rather than two spellings of them.
Measured before this commit, and the run that covers it is
`flock /tmp/claude-1000/openrepotools-pytest.lock python3 -m pytest tests -q`,
in flight as this is written:
python3 -m pytest tests -q round-1 tree: 573 passed, 0 failed (1406 s)
the shell suite inside it: 1781 passed, 0 failed, 0 skipped, 0 pending
main 92eab6c 1500 passed · main 8345d2f (after #41) 1678 passed
this branch before the merge with #41 1595 passed
CI on `c42d962`: `tests`, `tests-no-submodule` and `tests-windows` green,
`tests-macos` still running at the time of writing — the macOS-only `--dir`
defect that made it red on `fce18d5` was fixed in `f00229d`, one commit later.
Lane: openrepotools-3 (openRepoTools-3)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…transcript never half-moves between profiles, and the launch line is quoted by something bash 3.2 and BSD `sed` both read — Copilot rounds 1 and 2, Amendment 17 Addendum 1, and the suite's own lock
THE macOS JOB'S THREE RED CASES WERE THIS BRANCH'S OWN, and the defect is one
line that has been in `lane-start` since before this amendment:
quoted() { printf '%q ' "$@" | sed 's/ $//'; }
Two defects in it, and only their combination is fatal. `printf '%q '` renders
differently under BASH 3.2 — what Apple ships, and what `tests-macos` runs
(`/bin/bash` 3.2.57 on macOS 26.6.2): bash 4.4 and later give a multi-line
argument back as `$'…\n…'`, one readable and runnable word, while 3.2 has no
such form and escapes every space and every byte of every multi-byte character
with a backslash of its own. And the PIPE made that fatal rather than ugly:
under `set -o pipefail` the function's status is the pipeline's, and BSD `sed`
in a UTF-8 locale (`LC_ALL=en_US.UTF-8` there) refuses bytes that are not a
valid sequence — `RE error: illegal byte sequence` — which is exactly what
3.2's `%q` makes of the em dash every document in this estate is written with.
`quoted` is a BARE command in the `--no-launch` branch, so `set -e` ended
`lane-start` there: exit 1, empty stdout, three assertions red and no other
platform able to see it. The one caller that passes a multi-line argument is
this amendment's own — the `--no-launch` print of a launch carrying the
handoff's top block as its first prompt — which is why it went red here and
nowhere before. It is now POSIX quoting of the file's own, in `lane-start` and
`lane-handoff`: safe words bare, anything else in single quotes with `'\''`
for its own. `restart` carries the same line and is untouched — #43 deletes
that file in flight and a conflict there costs it a round.
COPILOT ROUND 1 (`db206db`), the four inline findings:
* `lane-handoff:842` — the `--restart`/`--exit` gate tested only the
object-log write. The record is THREE writes: the ROW is what every other
lane reads this lane's state from, and the HANDOFF's top block is literally
the respawn's first prompt. It now gates on all three and each refusal names
the act that clears it.
* `lane-start:1692` — `readlink -f` is not stock macOS userland, so `real_of`
fell back to the UNRESOLVED path, two spellings of one directory compared as
two directories, and the shared-`projects/` case — every case on a
launcher-configured workstation — would have moved a transcript that never
needed moving. Now `( CDPATH=''; cd -P -- "$1" && pwd -P )`.
* `lane-start:1781` and `:1788` — the sibling `<uuid>/` move's status was
discarded and the pointer's write was suppressed. The `.jsonl`, its sibling
and the pointer are ONE TRANSACTION now: each step is checked, a step that
cannot complete puts back what the steps before it moved (`ft_rollback`) and
refuses, and the refusal names anything the rollback itself could not undo.
* DECLINED, both mode findings (`lane-handoff:1`, `tests/test_repo_hygiene.py:198`):
`git ls-tree db206db lane-handoff` → `100755`, and `git ls-files -s` says the
same today. What they read is that test's docstring describing the FIRST
Amendment 17(a) commit; it now names `e25b54c` as the commit that fixed the
mode and points at `git ls-files -s`. The same stale rationale in the A17
fixture is corrected.
From round 1's SUPPRESSED list: `lane-start:1776` (`--dry-run` moved the
transcript before reaching the exit that says it moved nothing),
`lane-handoff:612` (`handoff_written=1` set whether or not the write
succeeded), `lane-handoff:494` (a failed `mktemp` gave a WRITERS section with a
count and no entries), `lane-handoff:395` (the record's `transcript` came from
`$sess_uuid`, not the resolved `$uuid`, so a handoff from a bare shell wrote
`transcript none` about a conversation whose id the same line carries),
`lane-start:1687` and `:1719` (`$CLAUDE_PROFILES_HOME` ignored by both profile
walks, though this repository's own README documents it), `lane-start:1881`
(a failed `lane-agent` read launched the default agent, which is the one thing
that read's own refusal forbids — it now refuses and names `--agent`), and
`lane-start:1997` (a lane with NO ROW wrote `RESUMED` as its object log's first
line).
COPILOT ROUND 2 (`ac0c697`):
* `lane-handoff:303` — the read fence treated exit 2 as "no answer", but
`canon_lane` spends 2 on a register holding two rows that differ only by
case. `lane-handoff` fell through to the next rung and would have renamed a
window, written a record and respawned a pane for whichever lane it answered
with. The refusal is now asked for by its own words, as `lane-start:755-767`,
`lane-end` and `restart` ask for it, and relayed.
* `lane-handoff:360` and `:403` — the lane is canonicalised through
`canon-lane` BEFORE the window rename, so the row's spelling is what the
rename, the top block, the payload, the log's lane field and the restart line
all carry; and a `--lane` that is not a lane name is refused there rather
than after the window has been renamed.
* `lanes-edit.sh:3946`/`:3963` — `pause_subfields_check` accepted a payload
carrying exactly one of `agent`/`transcript`, and an empty value parsed like
an absent one. Both are refused now, with the value judged before the pair so
a malformed one is still refused for the malformation.
* `lanes-edit.sh:5723` — `--utc` was taken for every verb and only its shape
was checked; it is the late `PAUSED`'s seam and is refused for everything
else, because file order is what every state read means by "last".
* `lane-start:1899` — `lane-transcript` answers for the LANE, so `--agent
codex` over a record that says `agent claude` would have appended
`Codex <claude uuid>` to the row's cell. The id is carried only where the
recorded agent IS the agent being launched.
* `lane-start:1982` — the launched agent's name is exported, so a `codex`
session's own handoff does not record `agent claude`.
FILED, NOT FOLDED IN: #55 (`swapped`'s first field is the
LOG's spelling and not the register row's — Amendment 15's claim, landed on
`main` with #41, so it predates this diff) and #56 (the
cross-profile transcript-holder scan tests only `kill -0`, where
`lanes-edit.sh`'s one liveness contract tests three things). Both claimed by
this lane.
AMENDMENT 17 ADDENDUM 1, in force 2026-09-14T20:59:31Z (brettheap/new-workstation#40,
`26a0d5b`), whose adoption line names this issue:
* (h) the record names the KIND, and there are THREE: `--in-process` writes
`in-process`, `--restart`/`--exit`/`--late` write `respawn`, and a PLAIN
handoff writes `unknown` — the act that follows it is not this command's to
know. The word is in the payload (`; kind <k>`) and in the line's FREE TEXT
after the why (`clear in-process`, `clear respawn`, `kind unknown`), which is
the addendum's own spelling.
* (i) the WRITERS section is a list to COUNT, not a list to relaunch, and its
first line is the clause's own words under EVERY kind.
* (j) the kind says what to EXPECT and never what to do, so `relaunch every
writer below` and `relaunch NONE of them` are both gone from the template.
* (k) one worktree, one writer, in the first acts, with the retire act for a
second one found.
AND THE SUITE'S OWN LOCK (#51). Four `python3 -m pytest
tests -q` runs were live at once on this workstation on 2026-09-14, every one
past a guard copied into four briefs: `pgrep -af '…' | grep -v pgrep
>/dev/null` never waits, because `grep` under the harness is a shell FUNCTION
whose status is 1 when its stdout is `/dev/null`, and its pattern is unanchored
besides. `tests/run.sh` is that guard written ONCE — wait, take
`${TMPDIR:-/tmp}/openrepotools-pytest.lock` (`flock`, or a `mkdir` lock on
macOS, which has none), wait again inside it, then run the suite — named in
`AGENTS.md` and `docs/README-lanes.md` as the way to run it, parsed by the
macOS job with every other bash file here, and pinned by a hygiene test that
holds the lock path in both places.
Closes #51 (with #36).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
…wn no-fetch notice; two module comments now point at the lists instead of restating a stale file count (#34) (#53) * WIP #34: /restart's `lread` fence keeps a read's stderr and drops only the known no-fetch notice; two module comments now point at the lists instead of restating a stale file count Checkpoint commit at the coordinator's request, ahead of merging main's #41 (Amendment 15, canon_lane, 8345d2f) into this branch. `bash -n tests/test_lane_helpers.sh` and `python3 -m py_compile` on both modified Python test files are clean; the full `bash tests/test_lane_helpers.sh` run and `python3 -m pytest tests -q` are still pending (re-)run after the merge and are not yet reflected here. Contents, inherited from the predecessor's uncommitted work and reviewed against issue #34 (Copilot's final round on #26, review 5194970614): - `skills/restart/SKILL.md`'s `lread` fence no longer silences a read's whole stderr with `2>/dev/null`. It now captures stderr to a `mktemp` file, drops only the one known `LANES_NO_FETCH=1 — not fetching; reading origin/…` notice line (verified against its real text at lanes-edit.sh's own `log_sync`), and lets everything else on that stream reach the person; where `mktemp` itself fails, nothing is filtered and the whole stream passes through unfiltered rather than being silenced. - Two module docstrings that still described a four-file installer (tests/test_repo_hygiene.py, tests/test_openrepotools_command.py) now point at the lists the suite actually derives its counts from (`SHIPPED_BASH`/`LANE_BASH`, and `INSTALLED`) instead of restating a number — `--install` places eleven files today, and #43 (another worktree) is removing the `restart` COMMAND while this skill stays, so a restated number would go stale again. - Three new tests/test_lane_helpers.sh cases exercising the fence: the known notice is dropped and the answer still reaches the variable; a different stderr line on a read that still answers is passed through rather than dropped with the notice; and with no capture file at all (`mktemp` stubbed to fail) nothing is filtered. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * The `lread` fence's own grep breaks under a non-permuting getopt — the exact BSD shape it exists to survive — because its pattern sits before `--` Copilot's round-1 review of #53 (5202775204, review body, no posted thread) found that skills/restart/SKILL.md:58's new filter line — `grep -v 'not fetching; reading origin/' -- "$ln_err_file"` — puts the PATTERN before `--`. GNU grep hides this by permuting options past an operand; a non-permuting getopt (BSD's, every macOS shell) reads the pattern as the first operand and `--` itself as a second one, a FILENAME. Reproduced here without a macOS runner by setting `POSIXLY_CORRECT=1`, which turns GNU grep's own permutation off: $ POSIXLY_CORRECT=1 grep -v 'not fetching; reading origin/' -- /tmp/x grep: --: No such file or directory /tmp/x:<the file's own content, now prefixed with its name> Fixed the one line: `grep -v -e 'not fetching; reading origin/' -- "..."`, the exact form tests/test_repo_hygiene.py:2193 already documents for `sed` and `grep` alike. Tests: tests/test_lane_helpers.sh's #34 section gains a case that exports POSIXLY_CORRECT=1 around the fence's own `lread` and asserts (a) the read still answers, (b) the other stderr line reaches the person UNPREFIXED, exactly as the helper wrote it, and (c) grep never errors on `--` as though it were a missing file — red against the unfixed line (confirmed by hand before this commit), green after. Filed #54 and claimed it for this lane: the same shape is still live, untouched by this PR's diff, at restart:247 and lanes:311 — and test_no_shipped_bash_ends_its_options_after_an_operand does not catch either, because its line-splitting regex is not quote-aware and the pattern's own embedded `;` hides the `--` from the tokenizer that would otherwise flag it. Verification: bash -n on the shell test file, `bash tests/test_lane_helpers.sh` and `python3 -m pytest tests -q` both green (footers quoted in the PR). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… when a later write from the same shared checkout may already carry the commit (#32) (#50) * WIP #32: `commit_push`'s true-conflict die stops claiming "nothing was pushed" when a later write from the same shared checkout may already carry the commit Checkpoint commit at the coordinator's request, ahead of merging main's #41 (Amendment 15, canon_lane, 8345d2f) into this branch. `bash -n lanes-edit.sh` and `bash -n tests/test_lane_helpers.sh` are both clean; the full `bash tests/test_lane_helpers.sh` run and `python3 -m pytest tests -q` are still in flight / pending re-run after the merge, and are not yet reflected here. Contents, inherited from the predecessor's uncommitted work and reviewed against issue #32's five occurrences (2026-09-13/14, Eagle, several lanes writing the register concurrently) and its own comments: - `commit_push`'s true-rebase-conflict branch (lanes-edit.sh ~1048-1062) reworded from an unconditional "rebase conflict on origin/main — nothing was pushed." to "…— not pushed by this attempt; a later write from this checkout may already carry it, so look before you retry.": three of the five observed occurrences named a commit that WAS already on origin by the time anyone read the die, carried there by the very next `lanes-edit.sh` write out of the same shared checkout. The RECOVERY lines are reordered to fetch-and-look at origin first, and the diff/reset instructions are now conditional on "only if it is not there". - Two new cases in tests/test_lane_helpers.sh: RACE-RETRY-WINS (a second clone lands a peer commit between this invocation's own pull and its push; asserts the final message names the attempt that actually pushed, never attempt 1's rejection, and that the row's text is written once, not twice) and OURS-CONFLICT (the peer rewrites the exact same row, a real conflict; asserts the abort truly leaves the worktree clean, the new wording appears, the old unconditional "nothing was pushed." does not, and the look-before-reset ordering holds). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * `commit_push`'s exit-3 table and its recovery notes stop contradicting the die message they document, so a reader who already found the commit on origin is told to STOP instead of reset and redo it Copilot's round-1 review of #50 (5202640056) found two things this PR's own diff introduced without noticing: - lanes-edit.sh:188 (posted thread) — the exit-code table's row 3 still read "nothing was pushed, the edit is a local commit" after the die message a few hundred lines below it picked up the attempt-scoped wording ("not pushed by this attempt; a later write from this checkout may already carry it"). Reworded to match. docs/README-lanes.md:939 carries the same stale claim as the manual's own copy of the table and is reworded too. - lanes-edit.sh:1176-1181 (suppressed comment on :1180) — the RECOVERY block's "only if it is not there" qualifier was attached to the diagnostic `diff` line alone; the `reset --hard` and "redo the edit" lines after it stayed unconditional, so a caller whose fetch/log just showed the commit already on origin was still told, by those two lines, to reset and redo — the exact duplicate #32 exists to prevent. One new line between the look and the reset — "if that commit is already there, STOP — reset or redo now would write it a second time." — gates all three lines below it at once. Tests: - tests/test_lane_helpers.sh's OURS-CONFLICT case (~3600) asserts the STOP line's text and that it prints between the look and the reset it gates. - tests/test_repo_hygiene.py gains test_the_exit_3_documentation_agrees_with_the_die_message_it_describes, tying lanes-edit.sh's table and docs/README-lanes.md's copy to the die message's own wording so the three can't drift apart silently again. Verification: bash -n on both shell files, python3 -m py_compile on the test module, `bash tests/test_lane_helpers.sh` and `python3 -m pytest tests -q` both green (footers quoted in the PR). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * Round 2: the recovery's own "look" traded a capped `log -3` for a real ancestry check, and the manual's prose paragraph and this test's own blind spot got the same fix as the table Copilot's round-2 review of #50 (5203033893) found three more things, all downstream of round 1's own fix: - lanes-edit.sh:1177 (posted thread) — `git log --oneline origin/$LANES_BRANCH -3` is not proof of absence: four or more later writes can bury this attempt's commit below a three-line window while it stays an ancestor of origin, and the next line would then wrongly permit reset/redo. Replaced with an actual ancestry test: `git -C $LANES_REPO rev-parse HEAD` is captured right after `rebase --abort` (HEAD is back to this attempt's own commit there) into `cp_sha`, and the look becomes `fetch ... && merge-base --is-ancestor $cp_sha origin/$LANES_BRANCH && echo ALREADY-THERE || echo not-there`. The STOP line now reads "if that printed ALREADY-THERE" rather than depending on what a human sees in a capped list. - docs/README-lanes.md:939 (posted thread, actually about the prose two lines above the table at :185-190) — the manual's own conflict-recovery paragraph still told readers to diff/reset/redo immediately, with no fetch, no ancestry check, no STOP. Reworded to mirror the ordered recovery lanes-edit.sh prints. - tests/test_repo_hygiene.py:1910 (suppressed comment, no thread) — round 1's new test read only the table and the manual, so a regression in the `die` MESSAGE ITSELF back to "nothing was pushed" left both docs unchanged and passed anyway. The die is extracted and checked first now, so all three have to agree with the thing that actually runs, not merely with each other. Tests: tests/test_lane_helpers.sh's OURS-CONFLICT case now asserts the ancestry command and its ALREADY-THERE/not-there framing, that a real commit sha is named (not a placeholder), and the STOP line's new wording; the ordering check (look before reset) now keys off the ancestry line. Verification: bash -n on both shell files, python3 -m py_compile on the test module, `bash tests/test_lane_helpers.sh` and `python3 -m pytest tests -q` both green (footers quoted in the PR). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * Round 3: the ancestry check's own `&&`/`||` could fold a FAILED fetch into "not-there", and the exit-3 table still named only one of its three causes Copilot's round-3 review of #50 (5203261904) found one real bug and one real gap, both in round 2's own work: - lanes-edit.sh:1181 (posted thread, real bug) — `fetch && merge-base ... && echo ALREADY-THERE || echo not-there` is left-associative: a FAILED fetch also falls to the trailing `||` and prints the SAME "not-there" a genuine non-ancestor does, silently waving the reset/redo path through against a ref that was never refreshed — recreating the exact duplicate this whole PR exists to prevent. Fixed by grouping the merge-base-and-echo pair in `{ ; }`, so the trailing `|| echo FETCH-FAILED` only fires when fetch itself failed (the group then never runs, against a ref nothing refreshed). Verified by hand against a real bare repo for all three outcomes: ALREADY-THERE (commit is an ancestor), not-there (commit is local-only, fetch succeeded), FETCH-FAILED (fetch against a bad remote). The STOP line now names FETCH-FAILED as its own STOP-and-retry outcome. docs/README-lanes.md:194 carried the same "answers non-zero → safe to redo" gap in its prose and is reworded the same way. - docs/README-lanes.md:946 and lanes-edit.sh:189 (suppressed comments, real completeness gap) — exit 3 is not only the rebase-conflict path: `git_timeout_die`'s network timeout and the six-attempts-exhausted path against a peer's uncommitted file (lanes-edit.sh:1140) both call `die ... 3` too, and the table's own claim is "one table, no two meanings on one number." Both rows now name all three causes; the attempt-scoped rebase-conflict wording (#32, round 1) is kept verbatim as the first of the three rather than replaced. Tests: - tests/test_lane_helpers.sh's OURS-CONFLICT case gains assertions for the FETCH-FAILED outcome (its own line, its own STOP-and-retry wording) and the updated ancestry-line text. - tests/test_repo_hygiene.py's exit-3 documentation test is whitespace- normalized (a phrase can legitimately wrap onto a comment-continuation line now that the row names three causes instead of one line) and gains a check that both the table and the manual name the other two causes ("timeout", "peer's"), not only the rebase-conflict one. Verification: bash -n on both shell files, python3 -m py_compile on the test module, a hand-built bare-repo check of all three ancestry outcomes, `bash tests/test_lane_helpers.sh` and `python3 -m pytest tests -q` both green (footers quoted in the PR). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * Round 4: the ancestry check itself checked the wrong thing — a peer's rebase of the dangling commit changes its sha, so `git cherry` replaces `merge-base --is-ancestor` to compare by content instead Copilot's round-4 review of #50 (5203455553) found the deepest issue yet, squarely in the mechanism this whole PR is about: - lanes-edit.sh:1196 (posted thread, real bug in round 2's own fix) — `cp_sha` is captured right after `rebase --abort`, but the very peer write this recovery exists for (the branch just above this one: "the next write out of it pulls this dangling commit onto its own") carries it by REBASING onto a newer base, which gives the SAME change a NEW sha. `merge-base --is-ancestor $cp_sha origin/$LANES_BRANCH` checks the OLD sha, so it still answers "not-there" once the change is published under the new one — recreating the exact duplicate #32 is about, one layer deeper than round 3's fetch-failure bug. Replaced with `git cherry origin/$LANES_BRANCH $cp_sha | grep -c '^+'`, which compares by PATCH rather than sha and finds a rebase-carried commit either way. Verified by hand against a real bare repo: a peer rebases our dangling commit (sha changes, same patch) — `merge-base --is-ancestor` on the original sha answers "not-there" (the bug); `git cherry` on the same original sha answers 0, i.e. found (the fix). Re-verified for a genuinely local-only commit (1, not-there), a plain already-pushed commit with no rebase involved (0, empty `git cherry` output reads as zero `+` lines), and a fetch against a bad remote (its own FETCH-FAILED, never reaching a stale `git cherry`). Fetch and the content check are now two separate RECOVERY lines (matching `lane-start`'s own RV-B3 two-line shape) instead of one line chained with `&&`/`||`, since fetch's own success/failure no longer needs to gate a single compound exit code. - docs/README-lanes.md:196 (suppressed comment) and :949 / lanes-edit.sh:189 (open threads) — the same "SHA vs content" gap in the manual's prose, and a second, narrower completeness gap in both exit-3 rows: `git_timeout_die` fires for a timed-out PUSH (lanes-edit.sh:1112/1141/1160) or a timed-out PULL (lanes-edit.sh:1166), and round 3's own wording only ever named the push. Both rows and the prose are reworded: "a push or a pull", the table's opening no longer flatly claims "the commit stays local" (a hung push may already have landed), and the manual's recovery paragraph now describes the `git cherry` check instead of `merge-base --is-ancestor`. Tests: - tests/test_lane_helpers.sh's OURS-CONFLICT case is rewritten for the new two-line fetch/cherry recovery: the cherry invocation, that it names a real commit sha, the `grep -c '^+'` count, the fetch-then-check-then-reset ordering, and the simplified "if that printed 0, STOP" wording. - tests/test_repo_hygiene.py's exit-3 documentation test now requires both "push" and "pull" alongside "timeout" in each row, closing the gap a bare "timeout" check left open (round 3's own wording would have kept passing it forever). Verification: bash -n on both shell files, python3 -m py_compile on the test module, three hand-built bare-repo scenarios (rebased-carry, local-only, fetch-failure) for the cherry-based check, `bash tests/test_lane_helpers.sh` and `python3 -m pytest tests -q` both green (footers quoted in the PR). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…er row are not ONE lane — the lock types `/rename <lane>` into the pane, a person's rename is an OFFER answered at the next prompt, and a transcript two live processes hold is a refusal in the guard, in `lane-start` and in `lane-end --retire` (#52) * WIP (#25, predecessor's state at the resume): the name guard, the lock, the offer, `transcript-holders` and the second hook entry, as the session that was writing them left them — unverified, no suite run behind it THIS COMMIT PROVES NOTHING AND IS NOT A CLAIM THAT ANY OF IT WORKS. The session building Amendment 12 was ended mid-work and left five files modified in this worktree with nothing committed; this is those bytes, recorded before anything else is done to them, so that the review that follows has a base to diff against and so that a second interruption cannot cost the work twice. No `bash tests/test_lane_helpers.sh` and no `python3 -m pytest` has been run against it by the session making this commit. `bash -n` is clean on all five, which is the whole of the evidence: `lanes-edit.sh`, `lane-start`, `lane-end`, `openRepoTools`, `tests/test_lane_helpers.sh`. WHAT IS IN IT, by file and by act of the brief: * `lanes-edit.sh` (+927): the `guard` verb and `guard_run`'s (b) table — the scope exemptions (`agent_id`, a `cwd` outside `$PROJECTS_ROOT`), the triple printed once, the eight rows with one filled-in cure each, `guard_lock_rename` typing `/rename <lane>` into this pane, `guard_offer`/`guard_answer` under `$CLAUDE_CONFIG_DIR/lanes/offers/<uuid>` with yes/no/anything-else, and Amendment 18(h)'s refusal. `transcript_holders` (act 1b's read) with its four-valued verdict, `record_window_state` (Amendment 11(h)'s agreement rule), `lane_shaped`, `under_projects_root`, `rows_named_ci`, `lane_binding_utc`, `guard_rename_is_newer`, `guard_type`, and clause (f)'s `ssb_name_line` hung off `session_start_block`. * `lane-start` (+70): `dup_check`, called at the window-binding step and inside the row-resume branch. * `lane-end` (+148): `--retire <pid>` taking a duplicate of the row's OWN id, with the window's own session refused by name. * `openRepoTools` (+132): the `UserPromptSubmit` entry beside the `SessionStart` one, both merges staged before either is placed. * `tests/test_lane_helpers.sh` (+326): the `== Amendment 12 ==` section and the fake tmux's `send-keys` log, `%pane` targeting, `#{session_name}`, `#{pane_current_command}` and the opt-in sticky `rename-window`. WHAT IS NOT IN IT, and is the work after this commit: no run of either suite; act 2 (`lane-start --name` on every launch branch) unverified; the pytest side of act 5 (the installer's second entry, the artifact count 18 → 19, the README caps) absent — `grep -c eighteen` still answers for eighteen artifacts; act 6's `docs/README-lanes.md` section absent; Amendment 15's `canon_lane` not yet on `main`, so `rows_named_ci` here is a second spelling of that round's function and the two have to become one when #41 lands; and Amendment 18 Addendum 2 (the `restart` COMMAND becomes `lane <name>`) is not reflected in any message. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * WIP (#25, swap-imminent): the `set -u` crash that took the whole Amendment 12 section and every `lane-start` window binding with it is fixed, and the artifact count, the docs and the pytest half of act 5 are written — one shell run behind them, the next not yet made WIP BECAUSE NO SUITE HAS RUN SINCE THE FIX. The run that FOUND it is measured below; the run that proves it is not yet made, and neither is any `pytest`. THE DEFECT, and it was one line. `transcript_holders` never initialised `th_here_prof`, and `lanes-edit.sh` runs under `set -u` (line 292): pass 2's `[ -z "$th_here_prof" ]` is reached for the FIRST record that is not this window's, and an unset variable there exits the shell with `unbound variable`. Measured on the predecessor's state at `3befe7c`, `bash tests/test_lane_helpers.sh`: **1468 passed, 124 failed** — 73 of the failures the WHOLE of the new `== Amendment 12 ==` section, each one stderr reading `lanes-edit.sh: line 4961: th_here_prof: unbound variable`, and the rest every `lane-start` case that reaches step 3b's window binding, because `dup_check` reads that 1 as "the records could not be read" and refuses fail-closed (`the same lane live in THIS window is not a collision`, `lane-start does NOT refuse the window its own session is running in`, all four `veto` cases, …). `th_here`, `th_prof`, `th_wrc` and `th_final` are initialised beside it. THE COUNT, EVERYWHERE IT IS DERIVED OR STATED (act 3's second half): 18 -> 19, because `--install` now merges TWO entries into one `settings.json`. `tests/test_openrepotools_command.py` derives it — `ARTIFACTS = len(INSTALLED) + 2*len(SKILL_NAMES) + 2*len(COMMAND_NAMES) + HOOK_ENTRIES`, the `+ 1` that was hand-kept now a named `HOOK_ENTRIES = 2` — and `openRepoTools` states it in six places, README.md in two, `docs/README-lanes.md` in one. "THE SEVEN ARTIFACTS THAT ARE NOT ONE OF THE ELEVEN FILES" is eight. DOCS (act 6): `docs/README-lanes.md` gains "The name guard and the lock (Amendment 12)" before "Hand edits" — the triple, the (b) table filled in, the lock and its pane conditions, the offer and its three answers, Amendment 18(h)'s duplicate read and its three callers, clause (f)'s line, fail-closed with the one bypass, and the scope. README.md's install paragraph names the second entry, what it does, and that a person's own `UserPromptSubmit` hooks are left where they are; its cap moves 409 -> 417 with a dated entry that says what the eight lines bought. PYTEST (act 5's other half): seven cases in `tests/test_install_skill_and_hook.py` — the entry byte-identical and with NO `matcher`, the `|| true` that must NOT be on it asserted on its own, twice adds one entry, a differing `guard` entry refuses and places nothing, `~/projects/xFactory/guard.sh` is NOT a conflict (the verb collides, not the file — R-A9-14 one event over, and this estate's usage guard is exactly that string), the pair is ONE all-or-nothing merge, and one write places both. ALSO: the comment that resolved `lane-start` "the way `restart` finds `lanes-edit.sh`" no longer names a file Amendment 18 Addendum 2 is removing (#43); it names this file's siblings. WHAT IS LEFT: the shell suite re-run and a `pytest`; hardening the two hooks against a fatal inside the read (this crash exited `session-start` 1, against R-A8-1's "always exits 0", and would exit `guard` 1, which lets a prompt THROUGH — the opposite of clause (d)); act 2 verified done and to be reported; `git merge origin/main` once #41 lands. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * WIP (#25, before the Amendment 15 merge): both hooks fail CLOSED against a fatal inside their own read, and AGENTS.md no longer promises exit 1 for the one command that must answer 2 WIP BECAUSE THE SUITE RUN BEHIND IT WAS STOPPED at 431 lines, 0 failures, to merge `origin/main` under it — bash reads a script as it executes, and Amendment 15's branch rewrites `tests/test_lane_helpers.sh`. THE HARDENING, AND IT IS THE `th_here_prof` CRASH READ FOR WHAT IT ACTUALLY BROKE rather than only fixed. That crash was an unbound variable under `set -u`, which exits the shell where it stands — so `|| :` and `|| rc=$?` at the call site caught nothing — and the two hooks failed in the two directions neither is allowed to fail in: * `session-start` exited **1** and printed no block at all, against R-A8-1's "this hook never fails and always exits 0". Measured: `the SessionStart hook still exits 0 with the name wrong — it never refuses` → `expected [0], got [1]`. * `guard` would exit **1**, and a 1 does not block a `UserPromptSubmit` — only a 2 does. A fatal inside the guard let the prompt THROUGH, in silence, which is the one failure Amendment 12(d) forbids by name and the exact state the whole amendment exists to end. So the `guard` dispatcher arm runs `guard_run` in a SUBSHELL and maps every code but 0 and 2 to a 2, naming the failure and the one bypass; `ssb_name_line` runs `transcript_holders` in one, and `session_start_block` calls `ssb_name_line` in one. The effects that must cross a subshell all do — the offer file, `lane-start`, the register write, stderr — and the only thing lost is shell state nothing reads afterwards. This is clause (d) made true of the guard's OWN failures and not only of the reads it makes. AGENTS.md (act 6's second half): its lane paragraph promised, without qualification, that each of the four "refuses with exit 1 naming `openRepoTools wip init` where that file does not answer". `lanes-edit.sh guard` deliberately answers that same unreadable workspace with 2, for the reason above — so the sentence now carries the exception and says why it is a mechanism and not a taste. Four lines; the cap moves 224 -> 228 with a dated entry. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Amendment 18 clauses (d) and (e) get the comment that says WHERE they plug in and WHY they cannot be built yet, and three sentences this round left saying the opposite of what is now true FOUR CORRECTIONS, none of them behaviour, and every one of them a sentence a reader would act on. 1. AMENDMENT 18 (d) AND (e), the one thing #25's brief asks for that the predecessor's state did not carry. A comment block inside `guard_run`, at the point both reads belong — after (h)'s count, with the lane known and nothing yet judged — saying what each does and why neither is code here: (d)'s answer IS Amendment 17(a)'s handoff under `--exit` (#36) and (e)'s `PAUSED … on behalf of` is written by the `--force` writer Amendment 18 adoption act 1 adds (#38). A guard refusing on a line no writer in this estate can yet produce would be refusing on a read of a log that never carries it, and one that TYPED `/handoff --exit` into a pane where that flag does not exist would type a command that does nothing into somebody's session. 2. `transcript_holders`'s verdict table said the `here` test was "`record_is_here`'s tiers 1 and 2; its tier 3 cannot discriminate". It is tiers 2 AND 3, and it is TIER 1 that cannot: tier 1 asks whether the record's `sessionId` is `$CLAUDE_CODE_SESSION_ID`, and every candidate in this set carries the very id being tested — so on the asking session's own transcript it would answer `here` for the DUPLICATE too, which is the one reading this function exists to prevent. The code was always right; the comment named the wrong two. 3. `session_start_block` carried, from Amendment 15's round, "AMENDMENT 12'S GUARD AND LOCK PLUG IN HERE (#25, NOT BUILT)". They are built, and the line immediately below the comment is clause (f). 4. `openRepoTools --help` and the install paragraph beneath it named "the SessionStart hook entry" and "ONE `SessionStart` entry" — the two places a person reads before running the command, and the only two left saying one where the count above them says two. Both name the `UserPromptSubmit` name guard, what it refuses, and that every `UserPromptSubmit` hook of the person's own is left exactly where it is. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The guard entry's conflict refusal says "NOTHING was installed" on ONE line, as its neighbour does — the phrase is the contract and a line break through the middle of it is a phrase no reader and no test can find FOUND BY THE TEST WRITTEN FOR THE BEHAVIOUR, which is the whole argument for writing it: `test_a_differing_guard_entry_refuses_and_places_nothing` asserted the same four things its `SessionStart` twin asserts and failed on the fourth — `assert 'NOTHING was installed' in '\nREFUSED: … not an installer's to do. NOTHING was\n installed. Remove that entry…'`. The refusal DID say it; it said it across a line break, so the sentence a person scans for and the string the suite looks for were both absent while the text read correctly to a human reading it slowly. THE PHRASE IS THE CONTRACT AND NOT PROSE. `--install` is all-or-nothing, and "NOTHING was installed" is the sentence that tells a person the bin directory, the skills, the command file and the OTHER hook entry are all untouched — it appears on ten other refusals in this file, every one of them on a single line. The paragraph is rewrapped around it rather than the phrase squeezed in: the "two refusals for one prompt" clause ends its own sentence, and the reason and the consequence go together on the next line. `python3 -m pytest tests/test_install_skill_and_hook.py -q`: 35 passed, 0 failed (28 before this round, 35 with Amendment 12's seven). Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The offer's `yes` puts THIS transcript in the new lane's cell, because `lane-start` may not — its veto 1 never moves a uuid another row records, and without that last write the guard refuses for ever on a state it created by obeying the person THE DEFECT, MEASURED, AND IT IS A LOOP. Clause (h) rule 2 names the end state and rule 3 repeats it: after a `yes`, *"window X, session X, ROW X STAMPED WITH THIS UUID"*. `lane-start --no-launch` does every other part of it and CANNOT do this one — its step 3b VETO 1 (Amendment 11 clause (d) rule 1) never takes a uuid that belongs to ANOTHER ROW, and after the rename this uuid still belongs to the lane being left. So it mints a fresh id instead. In the suite: `yes` moved the window to `repoGD-2`, whose row then read `STARTED by 7a01ae69…` while the session was `aaaa0012-1111…`. The NEXT prompt therefore finds a lane window whose row does not name this transcript — the last row of the (b) table — and refuses with `lane-start --no-launch repoGD 2`, which vetoes for the same reason and changes nothing. A blocking hook that refuses for ever, on a state it created by obeying the person, is the worst outcome this surface has. THE VETO IS RIGHT AND STAYS. It exists for the take nobody asked for — Evidence 2(b), *"`lane-start openXfactory-5` typed from a window named `openRepoProject-1`"* — and what clause (h) rule 4 forbids is moving a uuid between rows WITHOUT the person's `yes`. Here there is one, on the record, answered at this very prompt. So `guard_bind_uuid` makes that one write after `lane-start` returns, with `lane-start`'s OWN anchor discipline unvaried: the anchor is the PUBLISHED last id, so a checkout whose copy of the row is older than the one that landed refuses rather than writing a cell that no longer matches; a cell recording no uuid at all is anchored on `none recorded`, the one other text this act can meet; anything else prints the act and guesses at nothing. Both cached copies of the register are dropped first — the variable and the file every subshell reads — because this is the one read in the file that happens AFTER a writer has moved the ref, and a row `lane-start` has just CREATED is not in the cache at all. AND TWO FIXTURES IN THE NEW SECTION THAT DESCRIBED A WORLD THAT NO LONGER EXISTS, both of which made real assertions pass over the behaviour they name: * `$G_OUT` IS KILLED hundreds of cases above this section (the liveness section's own teardown), so every Amendment 18(h) case here was writing a record for a DEAD pid — which `record_is_live` correctly rejects, so the guard exited 0, `lane-start` bound the transcript, and `lane-end --retire` answered 8. Twelve of the twenty failures were that one line. The section starts a live process of its OWN and kills it at its end, and the suite's `cleanup` trap takes it too. * `add-row` HAS REFUSED TWO ROWS FOR ONE NAME since Amendment 15 landed (#41), which is the point of the guard's own row for that state: the pair can no longer be created by the tooling, only met. So it is seeded the way it actually arrives — straight into the register and pushed — the same `add_seed_row` + commit the Amendment 15 section uses. TWO REGRESSION TESTS FOR THE TWO CONTRACTS THE `set -u` CRASH BROKE, each holding the pair of hooks to its OPPOSITE rule against ONE fixture: the same unreadable session records refuse the prompt (clause (d)) and still leave `session-start` exiting 0 with its whole block printed, down to the `(no fetch)` tail every branch ends with (R-A8-1); and a register this workstation cannot read blocks the prompt with a 2, never the dispatcher's 1 — the exception AGENTS.md now carries, and the one number that blocks a prompt at all. Docs: the manual's offer section states the last write and why `lane-start` may not make it. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The duplicate read answers about the two live processes it could not see — the holder of an id THIS WINDOW has no record of, and a second INTERACTIVE session in this profile — and the offer stops answering itself while one is live or running a `lane-start` it could not fill in COPILOT ROUND 1 ON #52: seven inline threads and five findings suppressed into the review body. FOUR ARE REAL AND ARE HERE; the other eight are declined on the PR with the evidence, one line each. Two of the four are checks that could not fire at all, which is the worst shape a fence has. 1. `lane-start`'s ROW-RESUME BRANCH COULD NEVER REFUSE, and it is the branch Amendment 18(h) names first: *"the id it is about to RESUME or bind"*. That call is reached only when `row_sid != win_sid` — so this window carries no record of the id being resumed, so `transcript_holders` answers `unrelated` for every live holder of it, and `dup_check` counted only `duplicate`. A `--fork-session` in a background pty host holding the row's own transcript was invisible to the one read written to see it, and `lane-start` would have launched the second resume that #39 was filed for. `unrelated` now blocks IN `dup_check` and nowhere else, because the question differs from the one the shared read answers: `transcript_holders` keeps `unrelated` silent so that a rule about a transcript cannot overturn Amendment 8(f)'s orphan (reported, never refused) or `forks`, both of which ask about a LANE. `dup_check` asks 18(h)'s own question about ONE id. At the window-binding call that id IS this window's session, so a `here` record exists and no verdict is `unrelated` — one rule, two call sites, no second definition of a duplicate. 2. THE COMPANION IS THE `bg` RECORD, NOT ANY RECORD IN THIS PROFILE. Amendment 8 ruling (g) names exactly one thing to pass over in silence: *"Beside the interactive process in the pane the harness runs a companion: `kind: bg`, no `tmux`, no `nameSource`, its own pid, and THE SAME `sessionId`"*. Keyed on the profile alone, a second live INTERACTIVE record beside this window's own read as that companion — and one profile resuming one transcript twice is the CHEAPEST way there is to make two live processes on one file: no move, no swap, one `--resume`. `record_fields_are_session` is this file's own predicate for the distinction and is asked here rather than re-spelled, so `bg` is a companion and `interactive` (or a harness too old to say) is a holder, in one reading for all three surfaces. 3. AN ANSWER IS A PROMPT. Clause (h) refuses *"every prompt in a process that shares its session id with another live one"*, and the offer's `yes`/`no` was being consumed in front of that count: a `yes` runs `lane-start`, writes a row and appends a uuid to a cell — out of a process that may not be writing at all, on an offer file the OTHER process could equally have answered. 18(h)'s refusal now stands in front of the answer and the offer is KEPT, to be answered at the first prompt after the duplicate is retired. 4. THE OFFER NEVER RUNS A COMMAND IT COULD NOT FILL IN. `lane_start_args` answers `--dir <path> <lane>` for a lane named before Rule 4's `<repo>-<n>` form — the register carries 22 of them — and that `<path>` is a PLACEHOLDER: nothing in the register, the window or the session says where such a lane's checkout is. Printing it in a diagnostic is honest; RUNNING it is not, and a `yes` did. `lane-start --no-launch --dir '<path>' legacy-ui` refuses on a directory that does not exist, and the offer would then ask the same question at every prompt afterwards. The offer now says what it cannot fill in, `yes` refuses with the act and the one word left to the person, and the offer is kept so `no` still answers it. AND THE TWO ROWS OF THE (b) TABLE THAT HAD NO CASE, found by reading the table against the section rather than by the review: **not inside tmux at all** (ratified decision D2) and a **tmux that answers nothing**, which is clause (d)'s indeterminate read and a different state with a different message. The first is the one cure in the table that cannot be filled in — which lane a windowless session belongs to is exactly what nothing there knows. TESTS, all in the Amendment 12 section: a same-profile INTERACTIVE twin refused by pid, window and profile, with the harness's own `bg` companion beside it still exiting 0 in silence, so the verdict is proved to turn on the KIND and not on the directory; a row whose cell records an id with a transcript here and a live `bg` holder of it in another profile, `lane-start repoGD 3 --no-launch` refusing 1 and launching nothing, then resuming it once that process is gone; a pending offer NOT answered while a duplicate is live, and still pending afterwards; a lane named `legacy-ui` offered, its `yes` refused with `--dir <that lane's checkout>` and the offer kept, its `no` answering; `TMUX=` refused with the window half empty and nothing typed; a `tmux` that exits 1 refused as a read that did not happen, with the `--safe-mode` bypass named. Docs: the manual's offer section carries the wait behind the duplicate read and the lane it cannot fill in; its duplicate-read section says what a companion is and that the kind and not the profile is the discriminator. AND TWO ASSERTIONS CHANGED, both with the citation and neither weakened. Amendment 8's `lane-start does not refuse a lane over an orphan` and `…so it cannot refuse a lane either` (a lone `bg` companion) now expect **1**, because the run they make next is `claude --resume <the row's id>` and a live process already carries that file. Everything else those two cases assert is untouched and still green: the orphan is REPORTED, named *"orphaned holder, not a rival"*, given 6(d)'s act, and the rival refusal (`take repoGH-2`) is still absent — the LANE is not taken by it; what is refused is the second resume of its TRANSCRIPT, which is 18(h)'s own sentence. Amendment 8(f)'s rule does not reach that state either: it governs *"a row's EARLIER session ids … still held by live but idle sessions"*, and this is the id the row ENDS on. Two `has` lines are added to each, so the refusal they now take is asserted rather than assumed. COUNTS. `bash tests/test_lane_helpers.sh`: **1777 passed, 0 failed** at `109186b` -> **1815 passed, 0 failed** here (38 assertions: 8 for the two (b) rows, 26 for round 1's four, 4 on the two cases above). `python3 -m pytest tests -q` is queued behind three other writers' runs on this workstation's one lock; CI runs the same suite on four jobs, and the round before this one was 578 passed, 0 failed. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The companion is the `kind: bg` record and nothing else is read as one, `--retire` refuses to name the other half of a live session as its duplicate, and the lock types into the pane tmux names when the record names none COPILOT ROUND 2 ON #52: three inline threads and five findings suppressed into the review body, none of them a restatement of round 1. FOUR ARE REAL AND ARE HERE; four are declined on the PR with the evidence, one line each. Two of the four are in code this branch wrote LAST round, which is the argument for reading a second round rather than closing after the first. 1. `not a session record` WAS TOO WIDE A TEST FOR "THE HARNESS'S COMPANION", and the counter-example is in this suite already: Amendment 11 clause (k)'s section writes an interactive fork with `kind: user`, and `record_fields_are_session` answers "not a session" for it. Under last round's spelling that record — and every kind a later harness invents — arrived at the guard as the benign `bg` companion beside this window's own, and two live processes on one transcript went unrefused. Amendment 8 ruling (g) names ONE shape to pass over in silence, so that shape is what the test matches: `kind = bg`. Everything else live on this id is a duplicate, including a record too old to carry a `kind` at all, which pass 1 writes down as `interactive` — the closed direction in both cases. 2. `lane-end --retire <pid>` SELECTED ITS TARGET BY PID AND NEVER READ THE VERDICT, so the one pid a person can reach for that is neither a duplicate nor the lane's own session — the harness's `kind: bg` COMPANION of that very session — was reported as a live duplicate and given Amendment 6(d)'s ending. That is the other half of the conversation that IS the lane. It is now refused by name, in ruling (g)'s own words, with `transcript-holders` named as the read that says which process is which and `lane-end <lane>` as the act that ends the lane. The guard and `lane-start` already passed it over; this is the third surface agreeing with them. 3. THE LOCK HAD A SESSION TO RENAME AND NO PANE TO TYPE INTO. The pane comes from the live record's `tmux` field, and the harness has been seen to write NONE for a process plainly in a window (Amendment 8 ruling (g)'s third fact) — a record `record_is_here` still places HERE, by the pane's own process tree. With an empty target `guard_type` returned 1 and the lock printed "tmux would not take the keys" for a name it could have fixed. Both callers run INSIDE the session's own pane, so `#{pane_id}` with no `-t` is that pane, asked of tmux rather than guessed, and M1's condition is still asked of whatever it resolves to. 4. M1's GATE HAD NO TEST, and it is the only barrier between this hook and somebody else's process: *"only while the pane's current command is `claude`"*. Both halves have one now — a pane running `bash` is not typed into and says so, and a pane whose command could not be READ is not typed into either, which is clause (d)'s direction one surface down. AND ONE LINE OF DOCUMENTATION THAT WAS A PROMISE THE CODE DOES NOT KEEP: `lane-end`'s usage offered `--retire <pid|uuid>` for both populations, and a duplicate cannot be named by a uuid — its uuid IS the row's own, so it could not pick between the processes carrying it. The usage and the option block now say which form names which, and the manual carries the same sentence. COUNTS. `bash tests/test_lane_helpers.sh`: **1815 passed, 0 failed** at `eedb55f` -> **1833 passed, 0 failed** here (18 assertions for the four findings above). CI at `eedb55f`: `tests` (the whole `pytest` suite) **passed in 12m15s**, `tests-no-submodule` passed, `tests-windows` passed, SonarCloud passed, `tests-macos` still running when this was written. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The guard's last write anchors on the OTHER text a session cell carries instead of a uuid, the lock never types into a pane a stale target names, and `--install`'s three planning refusals print both entries because the plan is a pair COPILOT ROUND 3 ON #52: one inline thread and five findings suppressed into the review body. THREE ARE REAL AND ARE HERE; three are declined on the PR with the evidence. This round is the last: its inline finding is round 2's own, narrowed one step further and declined for a reason that is about blast radius rather than about the shape of a record. 1. `pending — set by the session's first act` IS THE OTHER TEXT A SESSION CELL CAN CARRY, and it is exactly the one a `yes` to a fresh lane can leave. `lane-start` writes it into a row it creates with neither a minted uuid nor one it was allowed to take (`lane-start:1914`) — and after a rename, veto 1 refuses this window's uuid because the row being LEFT still records it. The guard's own last write (Amendment 12(h) rule 2's *"row X stamped with this uuid"*) anchored only on `none recorded`, so on that row it could not be made at all: the offer was consumed, the source was marked MOVED, and the next prompt refused for ever on a state the guard had created by obeying the person. That is the loop this branch fixed one commit ago, reached through the one cell text the fix did not list. Both texts are anchors now, and the case drives it end to end: a row whose cell carries no uuid, a `yes`, and the cell ending on this transcript afterwards. 2. THE LOCK TYPED INTO WHATEVER PANE THE RECORD NAMED, and `here` does not mean the record's target is this window: it is true by the target OR by the pane's own process tree, and tmux REUSES window ids. So a record that is here BY ANCESTRY could carry `othersess:@5.%5` — a pane some other session is sitting in — and `/rename <lane>` would be typed into it. M1's gate cannot catch that one, because that pane is running `claude` too. The pane is now taken from the record ONLY where its target names this window; otherwise it is left empty and `guard_type` asks tmux for the pane this hook is running in, which is the session's own. `ssb_name_line` takes the same fence, with `here_context` asked beside it because the read above it runs in a subshell. 3. `--install`'s THREE PLANNING REFUSALS DESCRIBED A PLAN THAT NO LONGER EXISTS: no `jq`, a `settings.json` that is not valid JSON, and a `hooks` that is not an object each said they would merge ONE `SessionStart` entry and printed one block. The plan has been a PAIR since Amendment 12 adoption act 3, and a person who cannot install reads exactly those three messages. All three name both entries and print both blocks. DECLINED, AND THE INLINE ONE IS THE INTERESTING DECLINE: that the companion test should also require no `tmux` target and no `nameSource`, the exact shape ruling (g) describes. It adds no detection — the duplicate this rule exists to catch, measured four times on 2026-09-14, is a `bg` record with neither field — and it adds a way for a HARNESS RELEASE to block every prompt on a workstation: this file's own `window_session` anticipates "the moment a harness starts writing a target on to companions", and on that day every lane session with a companion beside it would become "a second live process" and refuse every prompt until somebody edited the tooling. `kind` is the field ruling (g) makes the discriminator and `record_is_session` has implemented since Amendment 8; the other two are description. COUNTS. `bash tests/test_lane_helpers.sh` was **1833 passed, 0 failed** at `6cf5359`, and the figure for THIS round is measured on the merged tree in the commit that follows it: `main` moved to `b5244e2` while this round was being written (#44, the installer's mode stamping and unwritable-target refusal), which touches `openRepoTools` — a file this round also touches — so the suite run that means anything is the one after the merge. CI at `6cf5359`: `tests` (the whole `pytest` suite) passed in 10m42s, `tests-no-submodule` in 10m21s, `tests-windows` and SonarCloud passed, `tests-macos` still running when this was written; every check passed at `eedb55f` before it, macOS included. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * M1's gate is the one word M1 ratified — `claude`, and twelve panes measured on Eagle tonight say `node` bought nothing — and the `SessionStart` hook stops typing over a rename a PERSON made, which is the guard's to OFFER COPILOT ROUND 4 ON #52: two inline threads and three findings suppressed into the review body. THREE ARE REAL AND ARE HERE; two are declined on the PR with the evidence, one of them because it describes a state that cannot arise. 1. `node` IS NOT `claude`, AND M1 NAMED ONE COMMAND: *"only while the pane's current command is `claude`"*. This gate read `claude|node` on the reasoning that the harness is a node program and `#{pane_current_command}` reports the process rather than the wrapper. MEASURED tonight on Eagle, across the twelve panes tmux had: eleven lane panes report `claude`, two report `bash` (a session under a launcher wrapper) and one `zsh` — and NOT ONE reports `node`. So the second word bought nothing here and would have let a stray Node REPL sitting in a pane take `/rename <lane>` and an Enter as input. A pane this gate refuses is not a lock that failed: the caller prints the line for the person, which is the cure — and on this estate today the `bash` panes are exactly the windows where they will read it. 2. THE `SessionStart` HOOK WAS UNDOING THE PERSON'S OWN INSTRUCTION BEFORE ANYBODY WAS ASKED. Clause (f) types the rename for (h)1's case — DRIFT, a name that is no lane's — while (h) rule 2 makes a record whose `nameSource` is `user`, whose name is another lane's, and whose `nameSince` is later than this window's binding an INSTRUCTION, answered `yes` or `no` at the next prompt. This hook runs on every startup, resume, clear and fork, so a rename followed by a `/clear` had the name typed back before `guard_run` could put the question, and the offer never happened. It is SAID there now and left to the prompt that follows, in the same words the guard will use. 3. `tests/test_install_skill_and_hook.py`'s MODULE DOCSTRING still told its next reader that `--install` merges ONE `SessionStart` entry, three commits after this branch made it a pair and seven tests into the pair's own section. DECLINED. That the `bg` companion should be classified BEFORE the `here` branch: a `bg` record whose target is this window or whose pid is under this pane is this session's OWN companion — Amendment 8 ruling (g)'s *"one session, not a queue of rival holders"* — and `here` and `companion` are treated identically by every caller (both are passed over in silence), so the order changes no outcome; what the reordering WOULD do is make this window's own companion a `duplicate` on a workstation where `record_is_here`'s tier 3 is the only thing that answers. And that `record_window_state` should tell a silent tmux from a refuted window: `(gone)` and `(not asked)` already differ, `lane-end` protects the second, and separating a dead SERVER from a dead WINDOW needs a second question put to tmux — which is a read this file does not make anywhere yet and a change to `who`'s liveness reading as much as to this one, so it is named on the PR rather than written in a review round. COUNTS. `bash tests/test_lane_helpers.sh`: **1870 passed, 0 failed** at `e2b6694` -> **1875 passed, 0 failed** here. CI at `e2b6694`: every check green, `tests-macos` included (22m13s) — which is the run that proves the fixture fix this branch made for the three red lines it had there. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ndoff` on PATH, `/handoff` with `/swap`, `/lane-swap` and `/ctx`, the record naming the agent and its transcript, and `lane-start --agent` resuming a lane from any agent the table carries (#47) * WIP (swap-imminent commit, pushed unverified): a handoff is the swap under one name — `lane-handoff` on PATH, `/handoff` with `/swap` and `/lane-swap` as aliases, `/ctx` = `/handoff --restart`, the record naming the agent and its transcript, and `lane-start --agent` resuming the lane from any agent the launcher table carries WIP, AND THE WORD IS IN THE SUBJECT BECAUSE THE PROOF IS NOT IN YET. Every file parses (`bash -n` on the four commands and the suite, `py_compile` on the three pytest files) and the bash suite was green through every pre-existing section on the run before this one; the Amendment 17 section this commit adds had NOT yet been reached when the swap was called, and `python3 -m pytest tests -q` has not been run at all. Nothing here is merged on that basis: the next session re-runs both suites and takes what they say. lane-collision-protocol **Amendment 17** (ratified 2026-09-14T09:45:33Z, revision 2 with clause (f)), **Amendment 18(d)** and **Addendum 2**; #36, acts 1-8. THE ACT HAS ONE NAME AND THREE DOORS. `lane-handoff` on PATH (new file, `INSTALLABLES` 11 -> 12) is the `/lane-swap` skill's steps as one command: the identity triple fixed through `lanes-edit.sh`, the handoff file refreshed with a fresh Rule 3 top block, every running writer polled — `git status --short` and `git log @{u}..` of every worktree under the lane's checkout `.claude/worktrees/` and `<projects>/.lane-worktrees/<lane>/` — `PAUSED` written with Amendment 11(c)'s payload plus Amendment 17(b)'s `agent <name>; transcript <id|none>`, and the one restart line printed. The top block's template gains a WRITERS section (worktree, branch, brief, last commit, dirty and unpushed counts) and the first line *"relaunch every writer below from where it stands"*; it is PREPENDED and the file is never rewritten, so what was there stays below a `---` rule as history. `skills/handoff/SKILL.md` carries the steps with their history (a rename, so `git log --follow` reads straight through); `skills/lane-swap/SKILL.md` and `commands/swap.md` name it and add nothing, and `commands/handoff.md` and `commands/ctx.md` are new. `--exit` (Amendment 18(d)) types `/exit` into the lane's own pane — the pane from the LIVE record's own `tmux` field — because a handoff to another place is a handoff and not a restart. `--restart` is `/ctx` (clause (f)): `tmux respawn-pane -k` on that same pane through the launcher, with `LANE_START_FRESH=1 pclaude --lane <lane> <profile>` — never `restart <lane>`, which Addendum 2 takes off the person's PATH. BOTH REFUSE BEFORE THEY TOUCH THE PANE where the record could not be written: *"a pane is never respawned over an unrecorded lane"*. `lanes-edit.sh`, ADDITIVELY, because #41 is landing in the same file: `pause_subfields_check` validates the two sub-fields in the WRITER (`write_event`, from any caller) and reads them with `all` so that `agent not a key` is refused rather than read as `not`; `swapped` gains a sixth and a seventh field, EMPTY for every record written before the amendment; `window_lane`'s reader names that row's fields in full so a short list cannot swallow them; four new reads (`lane-agent`, `lane-transcript`, `lane-last`, `workspace-root`); and `log … --utc <UTC>` dates the late record by its own field. `lane-start --agent <name>` (clause (c)): the default is the agent of the last `PAUSED`, else `claude`. `claude` with its transcript here resumes by id exactly as Amendment 6 has always done; otherwise a launcher table — `claude` takes a named new session with the handoff's TOP BLOCK as its first prompt, `codex` takes the top block positionally, and a name the table does not carry is a refusal naming the two it knows. The Rule 3 stamp and the row's session cell are written BEFORE the launch, the stamp naming the agent (`RESUMED by codex <id>`) and the cell in that agent's own spelling (`Codex <id>`). ACT 6, the transcript follows the lane, shrunk to what the measurement of 2026-09-14T12:35Z left of it: `lane-start` SAYS which projects directory it found the transcript in and whether that directory is SHARED (`readlink -f` equal across profiles), and MOVES the `<uuid>.jsonl` and its sibling `<uuid>/` only where another profile's `projects/` really is another directory, leaving the pointer `<uuid>.jsonl.moved-to-<profile>-<UTC>` where they were. A uuid whose holder is LIVE in another profile is a refusal naming the pid, its window or `bg`, the profile and the retire act (Amendment 18(h)) — never a move, because a copy leaves two live transcripts of one uuid to diverge. ACT 7, `--late`, AND THE RULE IT IS BUILT ON. A lane's state is its log's last lane-kind line IN FILE ORDER and the log is append-only, so a late `PAUSED` appended after the relaunch's `RESUMED` would make a lane that is RUNNING read as paused. The simplest rule that keeps every `binding` and state read correct is to refuse to write a line that can no longer be last: `--late` REQUIRES `--at <UTC>` — a late line is dated by its own field — and is written only where the lane's last lane-kind line is a `STARTED` or `RESUMED` older than that instant. Every other case is a refusal that writes nothing, made BEFORE the writers are polled and before the handoff is refreshed, so a refused `--late` leaves the lane exactly as it found it. Stated in the code, in the skill and in the manual. Docs: `docs/README-lanes.md` gains "The handoff (Amendment 17)" — the act under its three names, the two sub-fields and the reads that answer for them, the launcher table, `/ctx`, `--late` with its rule, and the transcript rule; `README.md`'s install section moves to twelve files, three skills, three command files and twenty-five artifacts, with the cap raised in a dated entry. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * WIP (second swap-imminent commit, unverified): the anchor `lane-start` appends a non-Claude agent's id to is the PUBLISHED row's last id, and four Amendment 17 cases in the suite ask what they say they ask WIP FOR THE SAME REASON THE FIRST ONE IS: this is the predecessor's working tree at the moment the swap was called, committed so that nothing is lost and so that the next run's diff is against a tree that parses. The Amendment 17 section had not been through a green run when these lines were written; the suites decide what survives. `lane-start`: the id `append-session-id` is anchored on for an agent that is not Claude is `$row_sid`, the LAST id of the published session cell, which is the same anchor the minted-uuid append uses and for the same reason — `append-session-id` refuses unless its anchor occurs exactly once in that cell, so a stale copy of the row is a refusal that prints the act rather than a corruption. `$cell_anchor` is not that id: it is set only where a session is live in this window whose uuid the cell lacks, which is never the case for the launch of an agent that is not Claude, so the branch as written took the empty-anchor door every time. The suite, four cases that were asking something other than what they read: `lane-handoff`'s stdout is kept in `hf1_out` because a `swapped` read stands between the run and the assertions that quote it; the unwritable-record `/ctx` case is asked from a window NO live record names, so that there is no transcript uuid anywhere and `write_event` really does refuse (Amendment 11(e)) rather than being handed one by the window read; the shared-`projects/` case gets its own lane `repoSH-1`, because `repoFR-1`'s cell has since gained the uuid the `LANE_START_FRESH=1` run minted and that one has no transcript at all — the other branch; and the live-holder record is named for ANOTHER lane, which is the shape Amendment 18(h) is written about: a second process on one transcript that the lane's own liveness read does not see. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * `lane-handoff` is a command in the index and not only after `--install` chmods it: the executable bit, the macOS parse step and the mode of every placed file are each derived from a list the next file joins by existing THE PROOF THE TWO WIP COMMITS SAID WAS NOT IN YET. `bash tests/test_lane_helpers.sh` is **1630 passed, 0 failed, 0 skipped, 0 pending**, with the 129 cases of `== Amendment 17: the handoff is the swap ==` among them — the section that had not been reached when the swap was called. Nothing in the amendment's own code needed changing for that; what this commit carries is what reading the file alongside the suite found. `lane-handoff` WAS COMMITTED 100644. Every other file `--install` places is 100755 in the index and `repos.tsv`, which is data, is not. The installed copy runs anyway — `--install` stamps 755 on everything it places — so the defect has no symptom there; what it costs is `./lane-handoff` from a clone, and the `cp -p` this suite copies it with, which takes the mode git recorded. The suite's own case did not see it because the fixture ran `chmod 755` on the copy. That line is gone: the case now reads back the mode THE REPOSITORY carries, which is the only mode anyone else gets. AND THE THREE LISTS IT WAS MISSING FROM, each closed so that the next file joins by existing rather than by being remembered: * `ALL_BASH` gains it through a new `HELPER_BASH`, with the reason it is in neither list beside it stated where the other two state theirs — not `SHIPPED_BASH`, because `R-A11-11` (*a swap is never left unwritten*) is why it does not `set -e`; not `LANE_BASH`, because it carries no copy of Amendment 9(a)'s workspace resolver, resolving no workspace and reading the one path it needs back from `lanes-edit.sh workspace-root`. So 837 lines of shipped bash now take the parse gate, the LF index, the flag-spelling rule and the four bash-3.2 text rules; it passes all of them as written. * the macOS job's `/bin/bash -n` step, which is a HAND-WRITTEN LIST and the only place the README's bash-3.2 claim is asked of bash 3.2. `lane-handoff` was not in it and nothing said so: the job goes green and proves one file less than it claims. `test_the_macos_job_parses_every_bash_file_this_repository_ships` reads that step and holds it against `ALL_BASH`, per name, because `bash -n a b` parses `a` and hands `b` to it as `$1`. * the index mode, read out of the installer's OWN `INSTALLABLES=( … )` rather than a list in the suite — 755 for a command, 644 for the `.tsv` that is read and never run. That is the test that would have caught this the moment `lane-handoff` joined the installer's list, with nobody coming here first. THE COUNTS THAT STATE THEMSELVES, finished: `AGENTS.md` and `docs/README-lanes.md` still said eleven files, two skills and eighteen artifacts — twelve, three, three and twenty-five — and four docstrings in the installer's tests counted the same way. `README.md`, the installer's prose and every derived count moved with the first WIP commit; these are the ones that state rather than derive. lane-collision-protocol **Amendment 17** (in force 2026-09-14T09:45:33Z), **Amendment 18(d)** and **Addendum 2**; #36. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * The `/ctx` respawn types the word Amendment 18 Addendum 2 gives it — `lane <lane>` where that word is on PATH, the launcher where it is not, and never a `lane` this estate did not place Addendum 2 clause (i-8), in force 2026-09-14T16:50:32Z, says the respawn after `/ctx` *"relaunches the lane's own pane with `lane <name>` (its parked branch is exactly Amendment 11(i)'s act), or through the launcher directly"*, and its adoption line names this issue: *"#36 (`/ctx`) respawns with `lane <name>`"*. What this branch had was the second half of that sentence and never the first: every respawn was `pclaude --lane <lane> <profile>`. So `lane-handoff --restart` now asks PATH for the word and types it — `LANE_START_FRESH=1 lane <lane>`, with NO profile argument, because `lane <name>`'s parked branch reads the record this command has just written for the lane's directory and profile and asks nothing. The launcher line stays as the door the same clause leaves open, for the workstation where `lane` is not installed yet: it arrives with #43 and this act ships first, and a respawn is the one act no later refusal can undo — the pane's process is already dead when the new command fails — so the word is used only where this command can SEE it. AND NOT EVERY `lane` ON A PATH. `pclaude` is nobody else's name; `lane` is an ordinary English word, and a pane respawned over somebody else's `lane` is a pane the person cannot get back. The file is read for the string every bash file this toolset ships carries (`lane-collision-protocol`, in all six of them), and one that carries none is passed over for the launcher with a line saying which file was passed over — the safe side of the two, because it still starts the lane. The skill and the manual say the same thing in the same order, and the skill's snippet is now the choice the command makes in code rather than one branch of it. TESTS, in `tests/test_lane_helpers.sh` § "Amendment 17", all three on a PATH THIS FILE BUILDS: `a17_path_without_lane` drops every directory holding a `lane` from the section's PATH, so the cases ask their own question on a workstation that has the word (#43 places it in `~/.local/bin`) and on one that does not. `repoHF-8`, with a fake `lane` carrying the marker, asserts the respawn line is that file and `repoHF-8`, with the FRESH seam still on it, with no profile argument, and never the launcher; `repoHF-9`, with a fake `lane` carrying none, asserts the launcher line and the sentence naming what was passed over; `repoHF-5`, on a PATH with no `lane` at all, keeps every assertion it had. VERIFIED HERE: `bash -n lane-handoff`, `bash -n tests/test_lane_helpers.sh`. Suite before this commit, at e25b54c: 1630 passed, 0 failed, 0 skipped. THE RUN OF THE SUITE WITH THESE CASES IN IT FOLLOWS THIS COMMIT — it is pushed now because a context clear of the driving session is imminent and an unpushed change is a change the next session cannot see. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * The `/ctx` respawn reads the `lane` it found through a redirect, because the job that runs this suite under BSD `head` is the macOS one and a portability question is not one to answer inside the act that kills a pane `lane-handoff --restart` reads the head of the `lane` it found on `PATH` for the string every bash file this toolset ships carries, and read it as `head -c 8192 -- "$lane_word_path"`. The macOS job runs `python3 -m pytest tests -q` — which is this shell suite, through `tests/test_lane_helpers_suite.py` — under BSD `head`, whose option handling is its own; `head -c 8192 < "$file"` hands the file to the utility as a file descriptor and asks nothing of its argument parsing. Same bytes, same guard, one fewer question. Parsed: `bash -n lane-handoff`. The suite at f8a4ce6, with the three PATH cases this guard exists for, was 1821 passed, 0 failed, 0 skipped; the run over this line follows. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * A handoff names the KIND of clear it was written for, because an in-process clear does not kill this lane's writers — the top block runs `ListAgents` first and relaunches only what it does not name; and the two stale tests CI found in the same breath THE MEASURED FACT, in this lane on 2026-09-14: a harness `/clear` mints a NEW TRANSCRIPT ID IN THE SAME PROCESS. Every subagent the lane has running survives it — only the tool calls they had in flight die, a `Bash` killed that way exiting 137 — and the handoff written before that clear said *"every agent dies with this context; relaunch every writer"*, which was FALSE. A next session that believed it would have put a SECOND writer on a worktree the first one still held, and two writers on one worktree is how the work in it is lost. Only a NEW PROCESS takes the writers with it: the pane respawned (`--restart`, which is what `/ctx` does here), the session ended (`--exit`), or the launcher run after a plain handoff. So the act now records WHICH IT WAS and the top block tells the next session what to do about it: * `lane-handoff --in-process` — the clear that keeps this process. Refused with `--restart` and `--exit`, because each of those replaces the process every writer is a child of. Default, and every other form: `respawn`. * the `PAUSED` payload carries `kind <in-process|respawn>` after `agent` and `transcript`, on the same argument those two were added on: a reader written before the field stops at the sub-field it knows. It is in the RECORD and not only in the handoff because the record is what a later reader and a launcher have. * the top block's act (3) is now, in both kinds: **RUN `ListAgents` FIRST. A WRITER STILL LISTED IS ALIVE AND OWNS ITS WORKTREE: DO NOT RELAUNCH IT. Relaunch only writers `ListAgents` does NOT name, and only after `git status --short` and `git log @{u}..` in that worktree say where it stood.** The `WRITERS` section repeats the rule where the list is, and the kind adds its own sentence — *expect `ListAgents` to name none of them* for a respawn, *expect it to name every one, and relaunch NONE* for an in-process clear. The skill, `commands/ctx.md` and the manual say the same in the same order, and the skill carries the table of the two kinds so that a session writing the block by hand writes the same two texts. AND THE TWO TESTS CI FOUND, both of which were green here for the wrong reason: * `test_the_swap_skill_snippet_and_its_prose_spell_the_same_command` read `skills/lane-swap/SKILL.md`, which Amendment 17(a) turned into an ALIAS that restates no step — so it was asserting over an emptiness. It now reads the installer's own `SKILL_NAME`, which is where the canonical skill is declared once, and every assertion it makes lands on the file that carries the act (2 `restart_cmd=` branches, no `pclaude run` unexplained, `--lane` leading). * `test_no_committed_file_names_a_host_absolute_path` caught three `/home/b/` spellings in the manual's examples. They are `~/projects/repoHF` and `~/.claude/projects` now, which is what every other example in that file uses. AND ONE SUITE AT A TIME, in `AGENTS.md` where an agent is sent to read it: six lanes build in sibling worktrees of this checkout and FOUR SUITES RAN AT ONCE under the guard they had been given — `pgrep -af 'python3 -m pytest' | grep -v pgrep >/dev/null` never waits where `grep` is the harness's own shell function (status 1 when its stdout is `/dev/null`), and its unanchored pattern matches the guard's own command line. The working form is an anchored `pgrep -fc` with its pattern split so it cannot match itself, then `flock` so the waiters cannot all start together. The cap entry for those nineteen lines is in the test that holds it. SUITE: `bash tests/test_lane_helpers.sh` — 1821 passed, 0 failed, 0 skipped at `f8a4ce6` (the merge of Amendment 15); **1844 passed, 0 failed, 0 skipped** with the `kind` cases of this commit (+23: the record's kind, the block's ListAgents rule and its two texts, the `--in-process` record and block, the pane untouched, and both refusals). `bash -n` on `lane-handoff` and the suite. The pytest run that would have covered the two test files above is NOT started: this workstation has three suites running under other lanes and this lane is shutting down on Brett Heap's word; CI on #47 is the authority and has already run these two tests red on their pre-fix bytes. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * A pane is never respawned over a record that is only part written, a transcript never half-moves between profiles, and the launch line is quoted by something bash 3.2 and BSD `sed` both read — Copilot rounds 1 and 2, Amendment 17 Addendum 1, and the suite's own lock THE macOS JOB'S THREE RED CASES WERE THIS BRANCH'S OWN, and the defect is one line that has been in `lane-start` since before this amendment: quoted() { printf '%q ' "$@" | sed 's/ $//'; } Two defects in it, and only their combination is fatal. `printf '%q '` renders differently under BASH 3.2 — what Apple ships, and what `tests-macos` runs (`/bin/bash` 3.2.57 on macOS 26.6.2): bash 4.4 and later give a multi-line argument back as `$'…\n…'`, one readable and runnable word, while 3.2 has no such form and escapes every space and every byte of every multi-byte character with a backslash of its own. And the PIPE made that fatal rather than ugly: under `set -o pipefail` the function's status is the pipeline's, and BSD `sed` in a UTF-8 locale (`LC_ALL=en_US.UTF-8` there) refuses bytes that are not a valid sequence — `RE error: illegal byte sequence` — which is exactly what 3.2's `%q` makes of the em dash every document in this estate is written with. `quoted` is a BARE command in the `--no-launch` branch, so `set -e` ended `lane-start` there: exit 1, empty stdout, three assertions red and no other platform able to see it. The one caller that passes a multi-line argument is this amendment's own — the `--no-launch` print of a launch carrying the handoff's top block as its first prompt — which is why it went red here and nowhere before. It is now POSIX quoting of the file's own, in `lane-start` and `lane-handoff`: safe words bare, anything else in single quotes with `'\''` for its own. `restart` carries the same line and is untouched — #43 deletes that file in flight and a conflict there costs it a round. COPILOT ROUND 1 (`db206db`), the four inline findings: * `lane-handoff:842` — the `--restart`/`--exit` gate tested only the object-log write. The record is THREE writes: the ROW is what every other lane reads this lane's state from, and the HANDOFF's top block is literally the respawn's first prompt. It now gates on all three and each refusal names the act that clears it. * `lane-start:1692` — `readlink -f` is not stock macOS userland, so `real_of` fell back to the UNRESOLVED path, two spellings of one directory compared as two directories, and the shared-`projects/` case — every case on a launcher-configured workstation — would have moved a transcript that never needed moving. Now `( CDPATH=''; cd -P -- "$1" && pwd -P )`. * `lane-start:1781` and `:1788` — the sibling `<uuid>/` move's status was discarded and the pointer's write was suppressed. The `.jsonl`, its sibling and the pointer are ONE TRANSACTION now: each step is checked, a step that cannot complete puts back what the steps before it moved (`ft_rollback`) and refuses, and the refusal names anything the rollback itself could not undo. * DECLINED, both mode findings (`lane-handoff:1`, `tests/test_repo_hygiene.py:198`): `git ls-tree db206db lane-handoff` → `100755`, and `git ls-files -s` says the same today. What they read is that test's docstring describing the FIRST Amendment 17(a) commit; it now names `e25b54c` as the commit that fixed the mode and points at `git ls-files -s`. The same stale rationale in the A17 fixture is corrected. From round 1's SUPPRESSED list: `lane-start:1776` (`--dry-run` moved the transcript before reaching the exit that says it moved nothing), `lane-handoff:612` (`handoff_written=1` set whether or not the write succeeded), `lane-handoff:494` (a failed `mktemp` gave a WRITERS section with a count and no entries), `lane-handoff:395` (the record's `transcript` came from `$sess_uuid`, not the resolved `$uuid`, so a handoff from a bare shell wrote `transcript none` about a conversation whose id the same line carries), `lane-start:1687` and `:1719` (`$CLAUDE_PROFILES_HOME` ignored by both profile walks, though this repository's own README documents it), `lane-start:1881` (a failed `lane-agent` read launched the default agent, which is the one thing that read's own refusal forbids — it now refuses and names `--agent`), and `lane-start:1997` (a lane with NO ROW wrote `RESUMED` as its object log's first line). COPILOT ROUND 2 (`ac0c697`): * `lane-handoff:303` — the read fence treated exit 2 as "no answer", but `canon_lane` spends 2 on a register holding two rows that differ only by case. `lane-handoff` fell through to the next rung and would have renamed a window, written a record and respawned a pane for whichever lane it answered with. The refusal is now asked for by its own words, as `lane-start:755-767`, `lane-end` and `restart` ask for it, and relayed. * `lane-handoff:360` and `:403` — the lane is canonicalised through `canon-lane` BEFORE the window rename, so the row's spelling is what the rename, the top block, the payload, the log's lane field and the restart line all carry; and a `--lane` that is not a lane name is refused there rather than after the window has been renamed. * `lanes-edit.sh:3946`/`:3963` — `pause_subfields_check` accepted a payload carrying exactly one of `agent`/`transcript`, and an empty value parsed like an absent one. Both are refused now, with the value judged before the pair so a malformed one is still refused for the malformation. * `lanes-edit.sh:5723` — `--utc` was taken for every verb and only its shape was checked; it is the late `PAUSED`'s seam and is refused for everything else, because file order is what every state read means by "last". * `lane-start:1899` — `lane-transcript` answers for the LANE, so `--agent codex` over a record that says `agent claude` would have appended `Codex <claude uuid>` to the row's cell. The id is carried only where the recorded agent IS the agent being launched. * `lane-start:1982` — the launched agent's name is exported, so a `codex` session's own handoff does not record `agent claude`. FILED, NOT FOLDED IN: #55 (`swapped`'s first field is the LOG's spelling and not the register row's — Amendment 15's claim, landed on `main` with #41, so it predates this diff) and #56 (the cross-profile transcript-holder scan tests only `kill -0`, where `lanes-edit.sh`'s one liveness contract tests three things). Both claimed by this lane. AMENDMENT 17 ADDENDUM 1, in force 2026-09-14T20:59:31Z (brettheap/new-workstation#40, `26a0d5b`), whose adoption line names this issue: * (h) the record names the KIND, and there are THREE: `--in-process` writes `in-process`, `--restart`/`--exit`/`--late` write `respawn`, and a PLAIN handoff writes `unknown` — the act that follows it is not this command's to know. The word is in the payload (`; kind <k>`) and in the line's FREE TEXT after the why (`clear in-process`, `clear respawn`, `kind unknown`), which is the addendum's own spelling. * (i) the WRITERS section is a list to COUNT, not a list to relaunch, and its first line is the clause's own words under EVERY kind. * (j) the kind says what to EXPECT and never what to do, so `relaunch every writer below` and `relaunch NONE of them` are both gone from the template. * (k) one worktree, one writer, in the first acts, with the retire act for a second one found. AND THE SUITE'S OWN LOCK (#51). Four `python3 -m pytest tests -q` runs were live at once on this workstation on 2026-09-14, every one past a guard copied into four briefs: `pgrep -af '…' | grep -v pgrep >/dev/null` never waits, because `grep` under the harness is a shell FUNCTION whose status is 1 when its stdout is `/dev/null`, and its pattern is unanchored besides. `tests/run.sh` is that guard written ONCE — wait, take `${TMPDIR:-/tmp}/openrepotools-pytest.lock` (`flock`, or a `mkdir` lock on macOS, which has none), wait again inside it, then run the suite — named in `AGENTS.md` and `docs/README-lanes.md` as the way to run it, parsed by the macOS job with every other bash file here, and pinned by a hygiene test that holds the lock path in both places. Closes #51 (with #36). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * A lock nothing can validate is stale and not eternal, a transcript never moves onto an occupied name, and the two acts that reach a handoff through a symlink resolve it the way macOS can — Copilot round 3 The round before this one went green on all four CI jobs, macOS included, which is what made these findings readable rather than hidden behind three red cases. `tests/run.sh` — THE LOCK THIS PULL REQUEST SHIPS AS THE WAY TO RUN THE SUITE, and three things a `mkdir` lock has to get right that the first shape of it got wrong (`tests/run.sh:109`, `:113`, `:120`): * A LOCK WITH NO PID IN IT IS STALE, NOT ETERNAL. The pid is written the instant after `mkdir` returns, and a run killed in that instant left a directory nothing could validate — which the loop waited on for ever, blocking the estate's one test entry point for everybody. * A TAKEOVER IS A CLAIM, NOT A DELETE. Two waiters that both saw a dead holder both `rm -rf`'d the name, and if one had already re-made it the other deleted the NEW holder's lock and both suites ran. It is a `mv` now — one rename, one winner, the loser loops — with the claimed directory RE-READ after the rename, because the only way a live holder can be behind that name is if it appeared between the read and the rename, and then it is put straight back. A stale state must also hold for three polls before it is claimed at all. * THE SIGNAL HANDLERS EXIT. `trap 'rm -rf …' EXIT INT TERM` released the lock on a Ctrl-C and then carried on running the suite, which is the collision the lock exists for. `lane-start:1878` — AN OCCUPIED DESTINATION IS REFUSED BEFORE ANYTHING MOVES. `mv <dir> <existing dir>` does not fail: it moves the source INSIDE it. A `<uuid>/` already in this profile would therefore have taken the other profile's sidecar as `<uuid>/<uuid>/`, been marked moved, and left the lane resuming with the transcript's state split from the transcript. Which of two `<uuid>` directories is this lane's is not a thing to guess at, so the read names what is there and stops — and a name that is NOT a directory still falls to the transaction below, where `mv` really does fail and the rollback puts the transcript back. `lane-handoff:812` and its twin in `lane-start` — `readlink -f` IS NOT IN THE STOCK macOS USERLAND, which is the same rule `real_of` took last round. Where it is absent the fallback hands the path back UNRESOLVED, so `hs_repo` — which git answers with a PHYSICAL path — and `hs_rel`, derived from the symlinked spelling, disagree; `git add` then refuses the pathspec and the handoff this act has just written, or the Rule 3 stamp it has just spliced, is left uncommitted. A handoff is reached through the estate's `handoffs` symlink on every workstation that has one. Both now resolve the PARENT with `cd -P`/`pwd -P`. `lane-handoff:1052` — `head -c 8192 … | grep -q` TAKES SIGPIPE UNDER `pipefail`. `grep -q` exits the instant it matches, `head` writing the rest of a word larger than 8192 bytes dies with 141, and the pipeline's status is that — so the estate's OWN `lane` was classified as somebody else's and `/ctx` respawned through the launcher instead of the word. `grep` reads all of the bounded input now, and the suite's fake `lane` is padded past 8192 bytes so the three PATH cases prove it. `lane-start:2132` — A LAUNCHER THIS WORKSTATION DOES NOT HAVE IS REFUSED BEFORE ANY WRITE. The row, the object log and the Rule 3 stamp are all written before the `exec`, so a `codex` that is not installed recorded the lane as RESUMED and then exited 127: a false transition in two append-only files that a person has to repair by hand. `lane-handoff:860` — the `window` sub-field's fence gains `; ` and `"`, the two its own comment already claimed it dropped. `window` is two space-separated refs in ONE sub-field, so a `; ` inside it is read back as the end of the sub-field and every reader recovers a window ref that is not the lane's. FILED, NOT FOLDED IN: #65 — the `/ctx` respawn recognises this estate's `lane` by a string in the word's own head, which is a heuristic and not provenance: any executable can carry it. Tightening that to the directory `--install` places these commands in is a change to the behaviour Amendment 18 Addendum 2 (i-8) adopted, and to three of this suite's PATH fixtures with it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The codex-absent case asks its question on a PATH this file built, because the workstation it was written on HAS codex installed Four assertions of the round-3 case `--agent codex on a workstation with no codex is refused` went red on Eagle and green on every CI runner, which is the shape of a fixture asking a different question rather than of a defect: the workstation carries `~/.npm-global/bin/codex`, so the refusal correctly did not fire and the launch went ahead. `a17_path_without <word>` is the generalisation of the `a17_path_without_lane` the three `lane` cases already use, and for the reason that helper's own comment gives — *a case that asked "what happens where there is no `lane`" against a workstation that has one would go green on the answer to a different question*. The codex case now builds its PATH the same way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The codex-absent case is not asked, and the test file says why: this estate installs that launcher in /usr/bin, so "a PATH without codex" is a PATH without bash The round-3 fix at `lane-start:2132` stands — a launcher the table knows and this workstation does not have is refused in 5a, before the row, the object log and the Rule 3 stamp are written, rather than after `exec` exits 127 and leaves a false PAUSED→RESUMED transition in two append-only files. Its case cannot be asked here. `a17_path_without codex` is how the three `lane` cases build their PATHs, and on this workstation `codex` is `/usr/bin/codex`, `/bin/codex` AND `~/.npm-global/bin/codex` — the launcher is installed globally on every lane workstation — so subtracting every directory that holds it returns the EMPTY string and takes `bash`, `git`, `awk` and `sed` with it: the run then dies at its own shebang with `/usr/bin/env: 'bash': No such file or directory`. That is why the four assertions were green on every CI runner (which has no codex) and red at the desk, which is the exact failure mode the `a17_path_without_lane` comment exists to prevent. So the reason is written where the case would have been, with what a fixture would have to express to ask it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The merge takes main's openRepoShape pin, which `git add -A` had put back `git add -A` while resolving the merge staged the submodule at the commit this worktree happened to have checked out (`ba14e1d`, this branch's base), which reverted `main`'s own pin bump to `39d5c98` (#46) in the gitlink while `contracts/openreposhape-pin.yaml` carried main's. The pin file and the gitlink move in ONE commit and never apart — AGENTS.md rule 3 of the pinned standard — so the gitlink is put back where main has it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The re-mode case names the skill it actually chmods, because Amendment 17(a) renamed that act and the case came from a branch written before the rename Both Linux CI jobs went red at `5c162df` — the merge of `main` at `b5244e2` — on one assertion of `test_a_skill_whose_bytes_are_right_and_whose_mode_is_not_is_re_moded`, and the defect is a name spelled twice in one file. `skill_paths()` DERIVES its two paths from `SKILL_NAMES`, so on this branch it hands back `handoff`'s — Amendment 17(a) renamed the act from `lane-swap` to `handoff` and kept `lane-swap` as an alias file — while the case that #40 and #44 added on `main` asserts the printed line with the literal `lane-swap:`. The installer was right: it chmod'ed the path it was given and named the skill that owns it (`handoff: … (mode restored to 644)`). The merge that brought a case written before a rename on to the branch that made it is exactly where a spelled name goes stale, and the file's own header says so five lines above the constant it should have used. Both assertions take `{SKILL_DIR_NAME}` now, which is the same derivation the paths take. Merges `origin/main` at `a954435` (#50, `commit_push`'s conflict die and its `tests/test_lane_helpers.sh` § #32) on top of `b5244e2` (#44, the installer's mode stamping and unwritable-target refusal; #53's `skills/restart/SKILL.md`). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The `--in-process` help says what to EXPECT, `--late --in-process` is refused as the contradiction it is, and a comment stops claiming a refusal `canon_lane` does not make — Copilot rounds 4 to 6 Three findings taken; the other ten are #72, filed with what each would cost, because every one of them hardens an act that already refuses rather than fixing something that writes the wrong thing. * `lane-handoff:65` and its `--in-process` usage line still told the reader the top block would say *relaunch NONE of them* — the instruction Amendment 17 Addendum 1 (j) removed from the template two commits ago. `usage()` PRINTS that comment block, so the command was handing a person the opposite of the adopted rule while generating the right block. Both now say the block tells the next session what to EXPECT, and that what to DO is clause (i)'s count. * `--late --in-process` was not refused, and the kind block takes the `in-process` arm first — so a late record could have said `kind in-process` and told the next session to expect writers live that died with the process the record is about. A late record is written for a session that has ALREADY ENDED; that is what makes it late. * And the comment over the `canon-lane` read claimed it refuses "a `--lane` this estate has no name for". It does not: `canon_lane` answers with the TYPED spelling where no row matches, deliberately, because `lane-start` uses the same resolver for a lane it is about to create. What that read refuses is an AMBIGUOUS register (15(d)) and a value that is not a lane name at all; a well-formed lane with no row is named by the writes below, and the comment says so now rather than promising a check that is not there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The documented guard counts the way the wrapper counts, and `pytest_live` stops reporting a failure in the same breath as a zero — Copilot rounds 7 and 8 Three threads on one inconsistency and one on its neighbour, all of them this branch's own: * `AGENTS.md` and `docs/README-lanes.md` showed the guard with `pgrep -fc`, which `tests/run.sh` deliberately does NOT use — `-c` is not in every `pgrep` this repository runs under, and it is the COUNT and never `pgrep`'s exit status that decides. A document that shows a person a non-portable form of the very guard the wrapper exists to make portable is worse than one that shows none. Both snippets count with `awk` now, as the wrapper does, and both say the wrapper is the canonical implementation and why the lines are repeated at all. * `tests/run.sh:66` — `pgrep` exits 1 when it matches nothing and `set -o pipefail` makes that the pipeline's, so `pytest_live` printed `0` and returned a FAILURE together. Every caller today reads the printed count, which is what its own comment says; the explicit `return 0` is what keeps that true for the first caller that uses it in a condition. The two remaining findings of those rounds — parametrising the skill-placement assertions over `SKILL_NAMES`, and an `INSTALLABLES=( … )` regex that tolerates a wrapped array — are #72's kind and are noted there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The skill records the KIND its command records, and asks `window-session` the question that read answers — Copilot round 9 Two conformance gaps between the two halves of ONE act, which is the thing Amendment 17(a) exists to prevent: `skills/handoff/SKILL.md` is `/handoff` and `/ctx` for a session with no `lane-handoff` on `PATH`, so a fact the command writes and the skill does not is a record whose shape depends on which door the person came through. * `skills/handoff/SKILL.md:476` — the skill's `PAUSED` payload carried Amendment 17(b)'s two sub-fields and not Addendum 1 (h)'s `kind`. A record from the skill was therefore one no reader could tell a surviving writer from a dead one by, while the top block it writes in the same act states the kind. The payload gains `; kind <k>` and the line's free text gains the same word after the why — `clear in-process`, `clear respawn`, `kind unknown`. * `skills/handoff/SKILL.md:612` — the `/ctx` respawn asked `window-session` with `#{window_id}` alone (`@21`). That read matches a live record on the HARNESS's own `tmux` field, `<session>:<@id>`, and the three spellings of one window are never derived from each other — `lane-start:975-984` says so and `lane-handoff` reads it that way. The skill's `/ctx` therefore found no record, left `$pane` empty, and respawned something that was not the lane's pane. It asks with `#{session_name}:#{window_id}` now. Three cases in `tests/test_lane_helpers.sh` § "Amendment 17", beside the ones that already hold the skill to the command's words. DECLINED, from the same round: `lane-start:2177`, that `--agent claude` should skip the recorded transcript. Amendment 17(c) is explicit the other way — *"`claude` with its transcript here resumes by id exactly as Amendment 6 has always done; every other case launches the named agent with the handoff's top block as its first prompt"* — and the suite's `--agent claude with no transcript here` case is the branch that takes a new session. `LANE_START_FRESH=1` is the seam for *a new session even though the transcript is here*, and it is what `/ctx` sets. FILED: `lane-handoff:924`'s late-record race is #72's kind and is noted there — the preflight reads the lane's last line before the poll and the refresh, and a `RESUMED` appended in that window would be overtaken by the `PAUSED` this writes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The skill's row flip reads BOTH state-cell shapes, because an ordinary fresh row is the one it did not know — Copilot round 10 `| WORD ·` is a state cell with history after it and `| WORD |` is one with none, and a fresh row from `add-row` is the second. The skill's parser knew only the first, so for those rows `$state` was empty, `replace-in-row` had no anchor, and `/handoff` through the skill left the lane RUNNING in the register with its handoff already refreshed and its PAUSED line already written — the one state this act exists to make impossible. `lane-handoff` has read both shapes all along and keeps whichever punctuation the row carries; the skill does the same now, including in the replacement it hands the writer. Two cases in `tests/test_lane_helpers.sh` § "Amendment 17", beside this round's other two. The round's other finding — a multi-line `why` reaching `append-line` and the top block before the event writer validates it — is #72's finding 6, filed in round 4 and noted again there with what `append-line` adds to it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…`/handoff` skill and its two command files, and the installer's TWENTY-SIX artifacts): six conflicts, and every one of them a list where `restart` leaves and `lane` stays The fourth merge of `origin/main` into this branch, and the one that touches the most of it: #47 adds a TWELFTH file to `INSTALLABLES`, a THIRD skill, TWO more command files and a suite section of its own, in the same four lists Amendment 18 Addendum 2 takes `restart` out of. THE SIX, and what each resolution is: 1. **`README.md`** — theirs, with the word changed: TWELVE files, and `lane` where their line says `restart`. The paragraph under it already said TWELVE. 2. **`openRepoTools`, the header inventory** — theirs, with `lane` named as `restart`'s replacement and Addendum 2 cited for why; their `lane-handoff`, three skills, three command files and twenty-six artifacts all kept. 3. **`openRepoTools`, `INSTALLABLES`** — the UNION and still TWELVE: `restart` out, `lane` in its place, `lane-handoff` beside it. One word left as another arrived, which is why the count did not move for Addendum 2 and moved by one for Amendment 17. 4. **`openRepoTools`, the artifact paragraph** — theirs (twenty-six, and the history of how it got there), with the one sentence naming `restart` as the second word on PATH rewritten to name both: *"`restart` then, `lane` since Amendment 18 Addendum 2 retired that one"*. 5. **`openRepoTools`, `SKILLS`** — theirs (`handoff lane-swap restart`), with the comment above it saying what the `/restart` skill is the inside half OF, now that the outside half is `lane <name>`. 6. **`skills/lane-swap/SKILL.md`** — THEIRS WHOLE. #47 replaces that skill's 495 lines with a 26-line ALIAS naming `/handoff`, which is Amendment 17(a)'s own act; this branch's edit to the old file was one sentence about the retired CLI, and the file it was in no longer exists in that form. Nothing is lost: the new `skills/handoff/SKILL.md` already says it, in `main`'s own words — *"**The respawn line is `lane <lane>` and never `restart <lane>`** (Amendment 18 Addendum 2, in force)"*. 7. **`tests/test_openrepotools_command.py`, `INSTALLED`** — the same union as `INSTALLABLES`, twelve entries, with the note that `lane` is `restart`'s place and `lane-handoff` is what makes it twelve. 8. **`tests/test_lane_helpers.sh`** — BOTH SECTIONS, THEIRS FIRST. Amendment 17's 935 lines and Addendum 1's 963 were inserted at the same anchor, immediately before `== the workstation seam ==`; the section that landed on `main` keeps the position it landed in, which is how the #41 merge resolved this same collision. WHAT NEEDED NO RESOLUTION AND IS WORTH SAYING: the CI parse list in `.github/workflows/tests.yml` merged to `lane` AND `lane-handoff` with no `restart`, and `lane-start` — which both sides rewrite, this branch at its bare form and #47 at `--agent` and the transcript move — merged clean. Measured on the merged tree, through `tests/run.sh` (#51's runner, which is the lock now): tests/run.sh 654 passed, 1 failed in 2129 s (35:29), and the shell suite inside it 2345 passed, 2 failed — TWO cases, and NEITHER is this merge's code: * `…quoted the way the shell reads it back` was THIS BRANCH'S OWN CASE pinning one spelling of a correct quoting. #47 replaced `lane-start`'s `quoted` — it was `printf '%q '`, rendering `team\ blue`, and is now a POSIX single-quote loop rendering `'team blue'`. Both are one word when a shell reads them back, which is the contract; the case asserts THAT now, accepts either spelling, still refuses the bare form, and proves the one-word property of both spellings in the same line. FIXED IN THIS COMMIT. * `one lane read alone is the same row the estate listing gives it` compares two `lanes` reads byte for byte, and one of the twelve fields is an AGE IN WHOLE MINUTES: `64h 49m` against `64h 48m`. It is on `main` and nothing here touches that read; a 35-minute run on a workstation building several lanes at once crossed a minute between the two. Filed as #75 and claimed by this lane, with the two-line fix named — compare the row without the column that is a function of the wall clock. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m
…le goes through the launcher in its own directory and profile, LIVE HERE attaches and never starts a second process, bound elsewhere names where; and `restart` leaves the person's PATH in the same breath (Amendment 18 Addendum 1 and Addendum 2) (#45) * WIP (#43, predecessor's state at the resume): `lane` written whole, `restart` deleted, and every surface re-pointed — committed unrun, so that what one agent left and what the next one changes are two diffs and not one NOT A CLAIM THAT ANY OF IT PASSES. The predecessor of this session was killed mid-work with fourteen files modified and none committed, and this commit is those fourteen files exactly as they were found — `git status` and `git diff --cached --stat` of the resume, to the byte, with nothing added, nothing rerun and nothing fixed. It is committed BEFORE the suite is run precisely so that the next commit on this branch is only what this session changed: a WIP squashed together with its repairs is a diff nobody can read against the ruling it answers to. WHAT WAS FOUND, file by file: * `lane` — 895 lines, new, 755. The word of Amendment 18 Addendum 1: the numbered pick, the three branches (available → the launcher in the lane's own recorded directory and profile; LIVE HERE → `move-window -a` + `select-window`, or `select-window` alone where the window is already in this session, or `tmux attach -t <session>` outside tmux, or `switch-client` under `--switch`; bound elsewhere → the refusal naming where), `lane <name> [<profile>]`, `--all`, `--dir`, `--dry-run`, and the no-terminal form that lists, suggests and asks nothing. * `restart` — DELETED, 545 lines. Amendment 18 Addendum 2, in force since 2026-09-14T16:50:32Z. * `lanes-edit.sh` — `lane_groups` and `lane_next_free` added after `lanes_rows`, both reading their rows from STDIN; their two subcommands `lane-groups` and `next-free` added, exempted from the workspace fence beside `session-start`, and named in the unknown-subcommand refusal; column 10 now spells `lane <lane>`. * `lanes` — its own next-free loop removed in favour of the helper's read, the `restart` hints re-pointed, `bind it:` in place of `restart:`, and a footer that says so where the helper predates the read. * `lane-start` — the bare form `exec`s `lane` (clause (i-4)). * `openRepoTools` — `INSTALLABLES` swaps `restart` for `lane`; ELEVEN STILL, because one word left as another arrived. * `.github/workflows/tests.yml` — the macOS bash-3.2 parse list follows. * `README.md`, `docs/README-lanes.md`, `skills/lane-swap/SKILL.md`, `skills/restart/SKILL.md` — the prose, including the manual's new "One word: `lane`" section and "Bind and list (Amendment 11, amended by Amendment 18)". * `tests/test_lane_helpers.sh` — +551/-177: decision 7's section re-pointed at `lane <name>`, and a new section "Amendment 18 Addendum 1: the word `lane`, the numbered pick, the three branches" with six seeded lanes covering the five states, a fake tmux of its own, and a pty harness that answers the one question for real. * `tests/test_openrepotools_command.py`, `tests/test_repo_hygiene.py` — the installed list, `SHIPPED_BASH`, `FORK_SURFACES`, the usage/parser parametrization and the lane-name fence test, all moved to `lane`. The base is `main` at 92eab6c. Amendment 15 (`canon_lane`) is PR #41 and is not on `main` yet; this branch is merged with it when it lands, and the deletion of `restart` wins over its edits to that file. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * WIP (#43): the one assertion description in the new section whose BACKTICKS bash ran — `lanes` was executed mid-suite and spliced its own footer into the line a person reads FOUND BY THE HYGIENE TEST THAT EXISTS FOR IT, and then seen in the suite's own output. `test_the_suite_descriptions_run_no_command_of_their_own` walks every `is`, `has`, `hasnt` and `skip` description in `tests/test_lane_helpers.sh` and refuses an unescaped backtick, because bash reads one as a command substitution: it was written after the `tests-macos` log of `29d3417` caught two such lines dying on `kill <pid>` and a third silently RUNNING `forks`. THE LINE WAS `tests/test_lane_helpers.sh:5662`, in the section Amendment 18 Addendum 1 adds, and it ran `lanes` FOR REAL in the middle of the run. What the operator saw in this session's own measurement was the description with the command's output in the middle of it: ok …while no lane of openRepoTools is recorded. next free position: 1 take it: lane-start openRepoTools 1 every lane: lanes --all still carries the closed lane, because that is the READ — five words of the description survived. The assertion itself passed and could not have failed: a description is not compared to anything, which is exactly why the three that came before it sat in the file unnoticed. python3 -m pytest tests/test_repo_hygiene.py tests/test_openrepotools_command.py -q before: 1 failed, 168 passed after: 1 passed (the named test), 123 deselected Committed as WIP at a coordinator's swap notice, with the shell suite mid-run; the run that proves the whole section is the next commit's evidence. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * WIP (#43): the container notice reaches `lane <name>`'s bound-elsewhere refusal too, which is the surface that needs it most — a person's OWN lane refused by naming a machine they are sitting at, with nothing on screen to say why THE NOTICE EXISTED AND ONE OF ITS TWO SURFACES HAD IT. With no `$LANES_WORKSTATION` inside a container the workstation is the CONTAINER'S id, so NO row is this workstation's — and the partition then puts every binding of this host under BOUND ELSEWHERE. `lane`'s listing has printed `lanes_workstation_why` beneath its rows since it was written, for exactly that reader; `lane <name>` reaches the SAME refusal without passing through the listing, and printed nothing. What a person met was lane openRepoTools-3 is bound on Eagle, window …, as of 2h ago, and this workstation cannot see another machine's tmux … on Eagle. Measured in this repository on 2026-09-14, in a container with the seam unset, which is the measurement the notice's own comment already carries. `go_elsewhere` now asks for it, on stderr because the refusal under it is, and `ws_notice` is SAID ONCE PER RUN rather than once per surface: the pick reaches both — the listing prints it under the rows, then the branch the picked row takes would print it again — and a person who picked a number out of a listing that had already explained itself does not need the paragraph twice. THE SENTENCE IS STILL ASKED FOR AND NEVER RESTATED: `lanes_workstation_why` is the one wording the writers share and this adds no copy of it. Tests, in the Amendment 18 Addendum 1 section: `lane repoPick-7` — a binding of THIS host, which is the row an unset seam turns into somebody else's — refuses with 2, names the workstation its row carries, carries the sentence, names the launcher whose job the value is, never names the `workstation:` key R-A11-14 rejected, and says it ONCE; and an ordinary elsewhere refusal with the seam answering carries no notice at all, so the notice means what it says. Shell suite before this commit, on the two commits below it: 1595 passed, 0 failed, 0 skipped, 0 pending. This commit is WIP at a coordinator's notice with the re-run still to make; it is made now so the `origin/main` merge that brings Amendment 15 in lands on a clean tree. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Copilot round 1, all eight taken: a `select-window` that FAILED is a refusal and not an attach onto another lane's window, a binding whose liveness could NOT be read is never launched, `--install` REMOVES the `restart` it once placed, and the flags the two aliases were handed are forwarded or refused rather than dropped EIGHT FINDINGS, EIGHT TAKEN, NONE DECLINED. Copilot's first review of #45 (2026-09-14, "🟡 Changes recommended", 3 inline and 5 suppressed). Each one below is a defect THIS pull request introduced, and each is proved by a case that fails against the commit before it. ## The two it called critical **`lane:610` — the select was part of the fence and was written as a courtesy.** All four attach paths put the terminal on the lane's OWN window, and three of them do something after it. The outside-tmux path spelled that select `|| :` and then `exec tmux attach`: a window that closed in the half second between the id-AND-session check and the act left the attach landing on whatever window that session had current — ANOTHER LANE — which is precisely what the fence above it exists to stop. One `select_window_or_refuse` now carries all four sites, naming what had already been done (a client switched, a window moved) so the refusal says where the person is. **`lane:777` — an unread session record was being read as a proof.** `lanes_rows` says on STDERR, and goes on answering, that this workstation's session records could not be read, in which case its STATE column is *"the LOG's verb alone: a lane that is live may show as IDLE or PAUSED here"* (`lanes-edit.sh:4628`). This command relayed that sentence and then grouped the row AVAILABLE and launched it — a second process on a lane that may still be running, which is the collision clause (h) is about and the fall-through Amendment 7(d) forbids. `read_rows` now catches the sentence out of the stderr it is already reading, and `go_available` refuses on it — for a BINDING alone: a PAUSED lane is parked by its own last line, needs no liveness proof, and launches exactly as it did. ## The moderate ones **`openRepoTools:234` — a word that left the list had not left the directory.** Dropping `restart` from `INSTALLABLES` stops THIS run from copying it and does nothing about the copy the last run placed, so every workstation that took the install before Amendment 18 Addendum 2 would have kept `restart` on its PATH for ever — against the addendum's own words, *"`restart` is no longer a command a person is given"*. `--install` now RETIRES it, and removes only what it wrote: the test is the header this installer puts in every command it owns, so a person's own script of that name is NAMED, left exactly where it is, and the `rm` that removes it is printed for them to run. **`lane:710` — the `f` answer dropped `--dir`.** The flag is in this word's own parser and `lane-start` takes the same one, so `lane --dir <path>` followed by `f` opened the new lane in a checkout nobody had named — Evidence 3's silent loss, at the one moment a lane is created. **`lane:701` and `lane-start:658` — PATH before the sibling, against this family's own rule.** `lane` states it fifty lines above its own `$LANES_EDIT` lookup: *"beside this command first … then on PATH. One resolution, so a workstation with two copies never reads one and writes the other."* Both new lookups had it the other way round. `lane-start`'s older `lanes` lookup took the same correction in the same commit, so the file carries ONE order rather than two. **`lane-start:661` — the alias dropped every flag it had been handed.** The parser takes `--dry-run`, `--no-launch`, `--confirm`, `--yes`, `--dir` and `--estate` before the zero-positional branch is reached, and the `exec` carried none of them: `lane-start --no-launch`, a flag whose whole promise is that nothing starts, became an interactive pick that launches. `--dry-run` and `--dir` mean the same thing in `lane` and go with it; the rest are REFUSED BY NAME, with the form that does take them filled in. A flag silently dropped is a promise that was not kept. **`lanes:432` — `|| :` collapsed every status of the `next-free` read.** The footer then told a person to take the install for a read that had FAILED. `2` is the un-upgraded workstation and still says so; anything else names the code it got and offers no position (Amendment 7(d)) — and a `0` that comes back with nothing a position can be made of is in that second class. ## And one this repository's own hygiene caught on top `grep -q "<pattern>" -- <file>` puts the `--` AFTER an operand, which BSD's `getopt` reads as a filename; `test_no_shipped_bash_ends_its_options_after_an_operand` named it in `lane` before any of it ran on macOS. Both new greps take `-e`, like the line above them that always had. ## Evidence python3 -m pytest tests/test_openrepotools_command.py \ tests/test_repo_hygiene.py \ tests/test_install_skill_and_hook.py -q before this commit 197 passed after 199 passed (the two the retirement adds) Shell suite on the commit BELOW this one, which is this branch merged with `origin/main` and before any of the above: 1781 passed, 0 failed, 0 skipped, 0 pending and for comparison, each a full run out of a clean extract of that commit: main 92eab6c (this branch's base) 1500 passed main 8345d2f (after #41) 1678 passed this branch before the merge 1595 passed The run that covers this commit is `python3 -m pytest tests -q`, in flight as this is written; it is committed now at a coordinator's swap notice and the footer goes in the pull request. New cases, all in `tests/test_lane_helpers.sh`'s Amendment 18 Addendum 1 section unless said otherwise: a `select-window` that fails on the outside-tmux path and on the move path (the section's fake tmux grows one knob, and the move is still in its log because it happened); a binding refused where the liveness read did not answer, with a PAUSED lane beside it still launching; `f` carrying its `--dir`, resolved; bare `lane-start` refusing `--no-launch`, naming every dropped flag rather than the first, and forwarding `--dry-run` through to the act `lane` then prints instead of performing; `lanes` listing where the `next-free` read FAILED without calling it an un-upgraded workstation. In `tests/test_openrepotools_command.py`: `--install` removing a pre-seeded `restart` that carries this installer's header, and leaving one that does not while printing the `rm`. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 1's own three misses: two refusals printed a command the helper would refuse, the footer named an exit code the read had not given, and the manual still said `--install` only STOPS placing `restart` THE ROUND THAT FIXED EIGHT THINGS INTRODUCED THREE OF ITS OWN, and every one of them is the same class of defect this repository already holds itself to: a refusal is a line a person TYPES, and a line they cannot type is a refusal that sent them nowhere. **`lane:553` — `lanes-edit.sh live-holder --lane <lane>` is not a command.** The liveness refusal added in the commit below this one printed it. `--lane` is `who`'s spelling (`lanes-edit.sh:who`, whose option loop takes it); `live-holder` takes a BARE lane — `lane="${1-}"; [ -n "$lane" ] || die "usage: live-holder <lane>"` — so what was printed would have been read as a lane named `--lane` and refused by `check_lane_name` for its leading dash. The neighbouring line in `go_elsewhere` is `lanes-edit.sh who --lane $R_LANE` and IS right, which is how the wrong one got written. **`lanes:479` — the reproduction piped the WRONG rows in.** `next-free` reads `lanes-edit.sh lanes`'s tab-separated rows on stdin, not this command's rendering of them, so `lanes --prefix <repo> | lanes-edit.sh next-free <repo>` would have handed the helper a listing it cannot parse and produced a second confusing answer on top of the first. It is the helper's own listing now: `lanes-edit.sh lanes --prefix <repo> | lanes-edit.sh next-free <repo>`. **`lanes:434` — the footer claimed an exit code the read had not given.** The new branch turned a `0` that came back with nothing a position could be made of into `next_rc=1` and then printed "exited 1" about a read that had exited 0. The reassignment is gone and the sentence says what actually happened — `exited <code> and gave no position` — with the 0-with-nothing case named in the same words rather than disguised as a failure. **And the manual now says what `--install` DOES.** `docs/README-lanes.md`'s Addendum 2 paragraph still read "no longer places `restart`", which was the whole truth before the round and is the smaller half of it after: the installer REMOVES the copy an earlier install placed, printing `restart: RETIRED` beside the file it took away, and removes ONLY what it wrote — a file of that name carrying this installer's own header goes, one that does not is NAMED, left exactly as it is, and the `rm` printed for the person to run. Measured by hand on this branch, both branches of it, `--install` run out of the checkout into a temporary HOME: restart: RETIRED, removed from …/.local/bin/restart (lane-collision-protocol Amendment 18 Addendum 2; `lane <name>` is that act now) $ ls …/.local/bin/restart → No such file or directory restart: RETIRED by lane-collision-protocol Amendment 18 Addendum 2, and …/instest2/restart is NOT this installer's copy — left exactly as it is. Remove it yourself if it is stale: rm …/instest2/restart $ cat …/instest2/restart → the stranger's script, byte for byte and in both runs the `/restart` SKILL is placed exactly as before, at both of its two paths — Addendum 2 clause (i-7): what left the PATH is the OUTSIDE half, and `/restart` was always the inside one. No assertion changes and none is weakened: the two texts are named by their distinctive words (`live-holder`, `exited`) and both still carry them. No cap moves — `README.md` 409 (cap 409), `AGENTS.md` 224 (cap 224), both untouched; `docs/README-lanes.md` has no cap and takes five lines. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Copilot round 2 on round 1, all four taken — the retirement no longer follows a SYMLINK a person made, the select's target names its session, a moved window's refusal names where it IS and not where it came from, and macOS's `/var` is why `tests-macos` was the only red FOUR FINDINGS ON MY OWN FIX ROUND, AND ONE ON THE PREDECESSOR'S, and the first of them could have deleted something somebody owns. ## Copilot round 2 (on `eb10256`), four findings, four taken **`openRepoTools:262` — `-f` FOLLOWS THE LINK.** The retirement's ownership test was `[ -f "$target" ] && grep -qF …`: a person's own `restart` SYMLINK pointing at a file that happens to carry this installer's header would have been UNLINKED by it. `plan_install_targets` refuses a `-L` target for exactly that reason — `unplaceable_kind` names the link rather than writing through it — and the retirement now holds to the same rule. `[ ! -L "$target" ]` first, so a link is NAMED, left, and the `rm` printed; both it and what it points at are still there afterwards, which is the new case. **`lane:666` — the select's target was an `@id` alone.** tmux resolves a bare `@id` by picking the "best" session holding that window, and PICKING is the one thing this command's id-AND-session fence exists to stop. Every select is `<session>:<@id>` now — in the acts and in the `--dry-run` lines that print them — so the act is the one the record described or it is nothing. **`lane:706` — after the move, the refusal named the SOURCE.** `move-window -a -s <src>:<idx>` puts the window in THIS session and a detached source with no windows left closes itself; the refusal under it still said "in session <src>" and offered `tmux attach -t <src>`, which is a session that may be gone. `select_window_or_refuse` takes the session it means, and after a move that is `$gl_this`. **`lane:589` — the no-directory recovery dropped the profile that was typed.** `lane <name> <profile>` on a lane whose record has a profile but no directory passes the profile check and lands on the directory refusal, whose one-run exit was `lane --dir <path> <name>` — following which walks straight back into the NO-PROFILE refusal. It carries `${profile:+ $profile}` now. ## And the one CI found that no review did `tests-macos` on `fce18d5` was the only red job: **1764 passed, 2 failed**, both in the predecessor's `--dir` case — FAIL …launching it under the profile that was typed, with the directory named FAIL …from that directory macOS's `$TMPDIR` is under `/var`, a symlink to `/private/var`. `lane` resolves `--dir` ONCE, where the operator typed it, because it `cd`s and then hands the same string to a launcher that would resolve it a second time from somewhere else — so the launcher sees `/private/var/...` while the case asserted the sandbox's own `/var/...`. Asserting the unresolved spelling is asserting that the resolution did NOT happen, which is the one thing that flag must do. The suite already had `$A11_DIR_R` for the relative-`--dir` case fifty lines down; it is computed ONCE now, beside `$A11_DIR` itself and AFTER the `mkdir` that creates it — a `cd` into a directory that does not exist yet falls back to the unresolved spelling and is the same defect written twice. The other three CI jobs on that commit were green: `tests`, `tests-no-submodule` and `tests-windows`. ## Evidence python3 -m pytest tests -q on the round-1 tree: 573 passed in 1406s (the shell suite inside it: 1781 passed, 0 failed, 0 skipped, 0 pending) and for the record, each a full run out of a clean extract of that commit: main 92eab6c (this branch's base) 1500 passed main 8345d2f (after #41) 1678 passed this branch before the merge 1595 passed New cases: the select assertions take the session-qualified target; the move's refusal is asserted to name the session the window is in NOW and never to send a person back to the source; and `--install` is run against a SYMLINK of a retired name, which must survive with what it points at. The run covering this commit is `python3 -m pytest tests -q`, in flight as this is written. A NOTE ON HOW THESE RUNS WERE SEQUENCED. The concurrency guard this work was briefed with — `while pgrep -af 'python3 -m pytest' | grep -v pgrep >/dev/null` — is a NO-OP in this harness: `grep` is a shell function from the session's zsh snapshot and returns 1 with stdout on `/dev/null` even when it matched, so the loop never waits. Measured: the same pipeline captured into a variable gives rc=0 and five matching lines. Four agents were running `pytest` at once past that guard. The form that works splits the pattern so it cannot match the guard's own command line: pat='python3 -m pyt'"est"; while [ "$(pgrep -f "$pat" | wc -l)" -gt 0 ]; do sleep 20; done Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Copilot round 3, all four taken — three states where the liveness proof had two, the `f` answer no longer vanishes from the question without a word, and a position printed by a read that then FAILED is not a position FOUR FINDINGS, FOUR TAKEN, and three of them are the SAME defect the round before fixed one file over: a read whose status was thrown away with `|| :` and whose emptiness was then read as an answer. Amendment 7(d) is the rule and it is one rule; taking it in `lanes` and leaving it in `lane` was taking half. **`lane:960` — the `f` answer vanished from the question with nothing on screen.** The picker's `next-free` read spent `|| :` on every status, so on an un-upgraded workstation — or a read that broke — the listing simply lost its new-lane action and the question narrowed to `[1-N, q]` with no explanation at all. It now says which: `2` is a `lanes-edit.sh` predating Addendum 1 and names the install; anything else names the code it got and the read to run by hand. Neither is a position of 1. **`lane:751` — `f`'s own read discarded its status too, and that one ALLOCATES.** A `next-free` that printed digits and then failed handed those digits straight past the numeric fence below it and into `lane-start`, which is a NEW LANE opened at a position this estate may well have taken. The status is read first now and any non-zero is a refusal naming it. **`lane:322` — THREE STATES, because "I could not read whether the proof was made" is not "the proof was made".** With no writable `$TMPDIR` the helper's notes go straight to the terminal and this command cannot read them back, and `LIVENESS_UNREAD` stayed `0` — so a binding whose liveness may never have been established was launched anyway, which is the collision clause (h) exists for and exactly the fall-through Amendment 7(d) forbids. `0` the proof was made · `1` the helper SAID it could not make it · `2` this command could not read whether it did; `go_available` refuses on `1` and `2` alike, in each one's own words, and a PAUSED lane — parked by its own last line — still launches under every one of them. **`tests/test_repo_hygiene.py:34` — the inventory's own description said "four".** `SHIPPED_BASH` has held six since this branch added `lane` beside `lanes`, and the paragraph above it still told a reader the macOS job parses four. A description that contradicts the list it explains is the drift these comments exist to prevent. New cases, all in the Amendment 18 Addendum 1 section: the pick listing where `next-free` is MISSING and where it FAILED, each saying why `f` is not offered and never offering a position; `f` answered on a failed read, refusing with 1 and starting nothing; and a binding refused under a `$TMPDIR` that cannot hold a temporary file, with a PAUSED lane launching beside it. The two `next-free` wrappers move up to the fixtures so the pick's cases and the listing's cases read the same two fakes rather than two spellings of them. Measured before this commit, and the run that covers it is `flock /tmp/claude-1000/openrepotools-pytest.lock python3 -m pytest tests -q`, in flight as this is written: python3 -m pytest tests -q round-1 tree: 573 passed, 0 failed (1406 s) the shell suite inside it: 1781 passed, 0 failed, 0 skipped, 0 pending main 92eab6c 1500 passed · main 8345d2f (after #41) 1678 passed this branch before the merge with #41 1595 passed CI on `c42d962`: `tests`, `tests-no-submodule` and `tests-windows` green, `tests-macos` still running at the time of writing — the macOS-only `--dir` defect that made it red on `fce18d5` was fixed in `f00229d`, one commit later. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * CI's one red case and Copilot round 4, six taken and two declined — a PAUSED row is the one an unread liveness proof is LEAST safe to launch, the move is fenced by the `@id` it checked instead of an index another window can be given, and an `-e` that skipped a DANGLING `restart` link left it on PATH THE RED FIRST. `tests` and `tests-no-submodule` were red on `9581ac9` with ONE case between them — `1832 passed, 1 failed` on both — and it is the round-3 commit's own prose reflow: pulling `$ga_why` out as a variable re-wrapped the refusal so that `PROVED` ended one line and `that binding dead` began the next, which the assertion reads as a substring of one line. The words are the fix, not the assertion. (`tests-windows` was green on that commit; `tests-macos` had not finished.) **`lane:560` — THE `PAUSED` EXCEPTION WAS A HOLE IN THE FENCE AT ITS WIDEST POINT.** Round 1 made an unread liveness read refuse a launch and let `PAUSED` through, on the reading that a lane parked by its own last line needs no proof. The sentence that refusal is REACTING to says otherwise, and this file already quoted it whole: *"a lane that is live may show as IDLE or PAUSED here"* (`lanes-edit.sh:4628`). The helper's own state machine is the second half — *"a live record naming one of the lane's ids beats the log's own last verb … otherwise the verb answers"* (`lanes-edit.sh:4700`) — so when the live read fails the verb is ALL that is left, and `/lane-swap` writes the `PAUSED` line while its session is still on screen. The row most likely to be running was the one row passed through. Every state refuses now; the two cases that asserted the launch assert the refusal, in each unread state's own words. **`lane:715` — THE MOVE IS SPELLED WITH THE `@id`, AND MEASURED.** The source was `<session>:<index>`, read one command before it was used. tmux 3.4 on a private socket, 2026-09-14: with `w3` at `src:2`, killing it, opening another window at that index and then `move-window -a -s src:2` MOVED THE INTRUDER into the destination and exited 0 — the wrong window, in this session, silently — while `move-window -a -s src:@2` refused with `can't find window: @2` and exit 1. The `@id` is what the fence above checked, tmux resolves it inside the named session or not at all, and the move is now a checked act: a move that failed is a refusal and the select under it never runs. The index read and its "tmux gave it no index" refusal are gone with it. **`lane:687` — UNDER `--switch`, THE SELECT COMES FIRST.** `switch-client` then `select-window` leaves a client that lost the race sitting in the target session's current window, which is another lane and this terminal already in it. `select-window` names the session it acts on and needs no client there, so the lane's own window is made current BEFORE the client moves; a select that fails now costs nothing, and a `switch-client` that fails is a refusal naming the half that did happen. **`lane:595` — A LANE WHOSE NAME IS NOT `<repo>-<n>` IS STARTED BY ONE ARGUMENT.** The no-directory refusal built its remedy as `<repo> <position>`, and a `--verbatim` lane has neither: `lane-start --dir <path> browser-ui-repair <n>` opens `browser-ui-repair-<n>`, A DIFFERENT LANE. `lane-start:601` takes that name as one argument beside a `--dir`, which is what the line prints now. **`lane-start:690` — THE VALUE GOES WITH THE FLAG THAT TOOK ONE.** `lane-start --estate x --yes` printed `lane-start <repo> <n> --estate --yes`, whose `--estate` swallows the `--yes` and names an estate `--yes`. The `--` words were dropped the same way. A line printed for a person to type is a line this command has to be willing to have typed back at it. **`openRepoTools:261` — `-e` FOLLOWS THE LINK AND IS FALSE FOR A DANGLING ONE.** A `restart` SYMLINK whose target has since gone — and the pre-move `link-estates` put exactly those links in a workstation's `~/.local/bin` — was neither removed nor named by the retirement: it stayed on PATH and the one run that was going to mention it said nothing. `-L` is the other half of "is there anything at this path"; what happens next is what happens to any link, which is that it is named and left. TWO DECLINED, BOTH MEASURED RATHER THAN ARGUED, because a decline that is only argued is one the next round makes again (the pattern this suite already uses for `ls --`): * **`tests/test_lane_helpers.sh:6424` — `env -C` is not a GNU-only option here.** The macOS job has run these very cases GREEN: job 104128188482 on `fce18d5` printed `ok a bare lane inside a checkout exits 0` and `ok answering with the number of an AVAILABLE lane exits 0`, both of them `env -C` forms, neither of which can exit 0 and carry a listing if `env` had refused the flag — and `main` has carried four more since Amendment 11. Asserted directly now, on every platform the suite runs on. * **`openRepoTools:263` — the header marker is the only ownership evidence an installer can have about a file it wrote BEFORE it knew it would retire it.** Durable metadata cannot answer for the population this act exists for: no installer that placed `restart` wrote a receipt, so no receipt can identify its output. The marker is this installer's own sentence, naming itself, at line 12 of the file it placed; the `! -L` fence (round 2) and the `-L` guard above keep it off anything that is not a regular file it can read. The receipt idea is real for FUTURE retirements and is filed as opensoft/openRepoTools#57. And three of round 4's suppressed comments — `lane:765`, `lane:969` and `lane:322` — were already fixed in `9581ac9`, one commit after the tree it reviewed. New cases, all in the Amendment 18 Addendum 1 section: a `move-window` that FAILED refusing with nothing selected; `--switch` with a select that failed never reaching `switch-client`, and a `switch-client` that failed refusing after the select that worked; a `--verbatim` lane's no-directory refusal naming the one-argument form and never a position; the `--estate` value in the alias refusal; `env -C` measured on this platform; and in `tests/test_openrepotools_command.py`, an install run against a DANGLING `restart` symlink, which survives and is named. The fake tmux learns the two acts that can fail after the fence has passed. Measured: `bash tests/test_lane_helpers.sh` — 1846 passed, 9 failed, where all nine are a concurrent writer on this workstation deleting the run's `$SANDBOX` and SIGTERMing its liveness fixture mid-run (`expected [0], got [143]`, then every read of a path under it): the last section and the two sentinel checks, none of them a case this commit touches, and every case of this section passed. The `python3 -m pytest tests -q` run under the workstation's lock is in flight as this is written; CI is the same command. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * Copilot round 5, three taken and six filed — a `next-free` that printed digits and THEN failed was still offering them one surface up from where round 3 stopped it, the acts a no-terminal run PRINTS dropped the `--dir` the `f` answer has carried since round 1, and the AVAILABLE heading now says what an unread liveness proof costs it THE SEEDED LANE WAS ALREADY SEEDED, which is what CI found on `f6200a05`: `1854 passed, 1 failed`, the one failure this branch's own new case. Line 373 has carried a `browser-ui-repair` row since long before this section, and line 808 RETIRES it — so a second row of that name is two rows differing only by case, which is `canon_lane`'s 2 (Amendment 15) and the refusal it prints rather than the one the case was written for. The `--verbatim` lane is `nav-rail-audit` now, a name this suite does not otherwise use, and the case reads the same. **`lane:1000` and `lanes:441` — A READ THAT PRINTED DIGITS AND THEN FAILED IS STILL A READ THAT FAILED.** Round 3 closed this in the `f` ANSWER (`go_new`, `lane:751`) and left it open in the two surfaces that OFFER `f`: both captured the status into a variable and then decided on the TEXT — next_free="$(… next-free …)" || next_free_rc=$? case "${next_free:-}" in '' | *[!0-9]*) next_free="" ;; esac — and text printed by a read that then failed passes a shape test on its own output. The fail-closed arm below was skipped for exactly the case it exists for, so `lane` offered `f` at a position nothing answered for and `lanes`' footer printed `take it: lane-start <repo> <that position>`. The status decides before the text does now, in both files, and the case that proves it is a fake that prints a position and exits 6 — `nfbroke` prints nothing, which is why neither surface had ever met this. **`lane:1030` — THE ACTS A NO-TERMINAL RUN PRINTS CARRY THE `--dir` IT WAS GIVEN.** The `f` answer has carried it since round 1 (`lane:710`); the line PRINTED for an agent to type dropped it, which opens the new lane in whatever checkout the reader happens to be standing in — Evidence 3's silent loss, at the one moment a lane is created. One surface performed the act and the other printed it; they spell it the same way now, `lane <name>` included. **`lanes-edit.sh:4829` — THE `AVAILABLE` HEADING SAYS WHAT AN UNREAD PROOF COSTS.** The partition is right and is one read: `lane-groups` groups a local row that is not LIVE as available, out of the STATE column — which is the log's verb alone when the session records could not be read. With the `PAUSED` exception gone (round 4, `lane:560`), EVERY row under that heading is then one `go_available` refuses, and a listing that numbered them without a word would be asking a person to pick an answer this command has already decided to refuse. The paragraph is printed under the heading it is about, names `live-holder` and `lanes --all`, and says that a LIVE HERE attach is unaffected because an attach starts nothing. It is silent where the proof was made. AND THE CODE STAYS ON THE WORD IT BELONGS TO: `lanes`' own footer wrapped between `exited` and the number, so a refusal whose whole job is to say WHICH code it got did not say it to anything reading a line at a time. That is the same defect as CI's red on `9581ac9`, where a reflow split `PROVED that binding dead` across two lines. SIX MORE OF ROUND 5's THIRTEEN ARE PROSE THAT SAYS "four" about inventories holding six, ten and eleven — `README.md:333` and `:403`, `.github/workflows/tests.yml:177`, `openRepoTools:370`, `tests/test_repo_hygiene.py:6` and `:2126`, `tests/test_openrepotools_command.py:9-11` and four more docstrings, `tests/test_lane_helpers.sh:30` and `:312`. Every one of them is ALREADY FALSE on `main` at `8345d2f`, where `INSTALLABLES` holds eleven and `SHIPPED_BASH` six: this branch renames one member of those lists and moves no count. Filed as opensoft/openRepoTools#58 and claimed by this lane, rather than folded into a diff that is already fourteen files. Two more of the thirteen restate findings this branch took in `f6200a05` (`lane:728`, `openRepoTools:261`). Measured, this tree, under no contention from the other suites this workstation was running: bash tests/test_lane_helpers.sh 1874 passed, 0 failed, 0 skipped, 0 pending CI on `f6200a05`: `tests` and `tests-no-submodule` red on the one seeding case above (`1854 passed, 1 failed`), `tests-windows` green, `tests-macos` still running at the time of writing. The `python3 -m pytest tests -q` run under this workstation's lock is in flight. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * Copilot round 6, one taken and one declined — the outside-tmux attach names the WINDOW and not only the session, so no act between the fence and the terminal can land on another lane, and an empty listing's `1` is measured to be the helper's own answer **`lane:708` — A SELECT AND AN ATTACH ARE TWO ACTS.** Outside tmux this word selected the lane's window and then ran `tmux attach -t <session>`, and a window that closed in between left the attach on whatever that session had since made current — another lane, which is the one thing the id-AND-session fence exists to stop. It is the same defect round 1 took in the select and round 4 took in the move, one act further along. MEASURED, tmux 3.4 on a private socket, 2026-09-14, with `dst` current on `@0`: attach -t dst:@2 attached AND made `@2` current, in ONE act attach -t dst:@3 (a window of another session) can't find window: @3 So the attach names `<session>:<@id>` now, in the act and in the `--dry-run` line that prints it. The select above it stays: it is this command's own refusal, in this command's words, while there is still a terminal to print it on — and the attach then resolves the same target at the moment it acts. **`lanes:424` — DECLINED, and measured rather than argued.** The `rc=8` branch prints `next free position: 1` without calling `next-free`, which round 6 read as a position "already taken" on a workstation whose helper predates that read. It cannot be: `lane_next_free` (`lanes-edit.sh:4847`) reads ONLY THE ROWS ON STDIN and keeps those whose name begins `<repo>-`, and the rows it would be handed are the ones that listing just found NONE of — while `lanes_rows`' own `--prefix` filter keeps every lane whose NAME carries the prefix (its third or-term). An empty filtered listing IS "no lane named `<repo>-<n>` in this register", and 1 is the only answer the helper could give. Routing it through that helper would add a FAILURE PATH in front of an answer that needs no read: an un-upgraded workstation would be refused the one position it can be certain of. Four assertions now hold both halves together — the listing's 8, its `1`, and `next-free` answering `1` over the same empty rows, twice. THE REST OF ROUND 6 IS THE TREE IT READ. It reviewed `f6200a05`, one commit behind: `lane:1048` and `lanes:440` (a `next-free` that printed digits and then failed) were taken in `39ec308`, and `lanes-edit.sh:4829` was answered there too — with a PARAGRAPH UNDER THE HEADING rather than a change to the grouping, deliberately. The partition is ONE read that `lane` and `lanes` share; moving a row out of AVAILABLE while the liveness is unknown would either hide a lane from the person who most needs to see it or invent a fourth group in a read two words render. What is true is that every row under that heading is one `go_available` will refuse, and the heading says exactly that, names `live-holder` and `lanes --all`, and says a LIVE HERE attach is unaffected. And five of its nine are the "four" prose filed as opensoft/openRepoTools#58 and claimed by this lane: `README.md:333` and `:403`, `openRepoTools:370`, `tests/test_openrepotools_command.py`'s module and fixture docstrings, and `tests/test_repo_hygiene.py:6`. Every one is already false on `main` at `8345d2f`, where `INSTALLABLES` holds eleven and `SHIPPED_BASH` six. Measured, this tree: bash tests/test_lane_helpers.sh 1879 passed, 0 failed, 0 skipped, 0 pending and the `python3 -m pytest tests -q` run under this workstation's lock covers this same tree. CI on `39ec308` was in flight as this was written; the run before it, on `f6200a05`, was red on one case only — the seeding collision `39ec308` corrects — with `tests-windows` green. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * Copilot round 7, five taken — the switch names the WINDOW like the attach now does, a position a lane has held is RESERVED and the comment that said otherwise was the drift, `repo-foo-1` is no lane of `repo`, a printed `--estate` value is quoted, and the one destructive step of `--install` is its LAST **`lane:738` — THE SWITCH NAMES THE WINDOW, for the reason the attach does.** Another client can make that session's current window something else between the select and the switch, and `switch-client -t <session>` would then land this client on it. MEASURED, tmux 3.4 on a private socket, 2026-09-14, with a client on `home` and `dst` current on `@1`: switch-client -t dst:@3 client moved to `dst` AND `@3` made current, ONE act switch-client -t dst:@999 can't find window: @999 (exit 1) **`lanes-edit.sh:4839` — A POSITION A LANE HAS HELD IS RESERVED, and the paragraph claiming otherwise was the drift.** It said positions "come back as lanes end"; `lane_next_free` has never done that — every row on stdin is taken, whatever its state, and the fixture proves it (`repoPick-5` ENDED, next free 6). The CODE is right and the comment was wrong: a lane's identity is its name, its object log `lanes/log/<lane>.md` is APPEND-ONLY, and a second lane at a retired position would write its life into the first one's file — so every read of that log would answer for two lanes at once. Positions are cheap; identities are not. The manual's reads table says it too, where it describes what that read answers. **`lanes-edit.sh:4853` — `repo-foo-1` IS NO LANE OF `repo`.** The prefix test was `substr(l, 1, length(rl))` against `<repo>-`, so a repository whose name is another name plus a hyphen took positions out of its neighbour: `next-free repo` would skip 1 because `repo-foo-1` exists, and hand out a number for a reason nobody could see in the listing. The name before the LAST hyphen is compared whole now, case-insensitively, which is the same rule the rest of this file reads lane names by (Amendment 15). **`lane-start:698` — A PRINTED VALUE IS QUOTED THE WAY THE SHELL READS IT BACK.** `--estate 'team blue'` printed bare is two words when it is typed again — a suggested command that does something else — and the `--` words had the same problem. Both go through `quoted`, which is the helper every other printed command in that file already uses. (The value being there at all is round 4's `lane-start:690`; this is its second half.) **`openRepoTools:369` — THE ONE DESTRUCTIVE STEP IS THE LAST ONE.** `retire_commands` ran BEFORE `place_skill_and_hook`, so a skills directory or a `settings.json` this run could not write left a workstation with the old word already DELETED and the adoption it was migrating to unfinished. Every other refusal in that file places nothing rather than half of it; this one now removes nothing until the rest of the install has actually happened. The case makes `~/.claude/skills` a regular file — a `mkdir -p` that cannot succeed — and requires the stale `restart` to still be there afterwards, with no `RETIRED` line claiming otherwise. New cases: the `--switch` log carrying `switch-client -t detsess:@32`; an `--estate` value with a space in it, quoted and never bare; `next-free` over a neighbour repository's rows (`repoNb-foo-1`, `repoNb-foo-2` → 1) and over its own under the same names (→ 2); the ENDED position asserted as RESERVED in those terms; and `test_install_retires_only_after_the_placement_it_migrates_to`. Measured, this tree: python3 -m pytest tests/test_openrepotools_command.py -q -k 'retire or dangling or migrat' 3 passed and the full `bash tests/test_lane_helpers.sh` footer is on the MERGE COMMIT above this one, because `origin/main` moved under this branch while these five were being taken (#44, the installer) and the run worth reporting is the run on the tree that lands. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * Copilot round 8, three taken and two declined — every act this word PRINTS is now a line that can be typed back: the `cd` is quoted, `--switch` survives into it, and a bare `lane-start`'s remedy carries the flag it KEPT and not only the one it refused Three surfaces print a command for a person or an agent to run, and each dropped something the run it describes would need. **`lane:612` — THE `cd` IS QUOTED WHERE IT IS A PATH.** The no-profile refusal hands over one line — `cd <the lane's dir> && pclaude --lane <lane> <profile>` — and a recorded directory with a space in it made that `cd` two arguments. `%q` through `quoted`, the same helper the dry-run uses; the PLACEHOLDER arm is left as words, because `\<the\ lane\'s\ checkout\>` is not a thing to type either. **`lane:1123` — `--switch` IS A DIFFERENT ATTACH AND NOT A PREFERENCE.** The no-terminal listing printed `lane <name>` with the flag dropped, and typing that back MOVES a live lane's window into the current session instead of switching the client to it — a different act on somebody's terminal. It goes on the `lane` line alone: `lane-start` has no such flag. **`lane-start:706` — THE REMEDY CARRIES WHAT IT KEPT.** It was built out of the REFUSED flags alone, so `lane-start --dir /work/repo --estate x` printed `lane-start <repo> <n> --estate x` and the checkout the person had named was gone from the line they were told to type. `--dry-run` went the same way. What is suggested is the whole command they meant. TWO DECLINED: * **`lane:721` — the outside-tmux attach `exec`s, and that is the contract.** This word is `exec`ed by the launcher and an attach must REPLACE this process rather than leave a shell waiting under the tmux client for the life of the session. What is left after round 6's fix is a window that vanishes between `select_window_or_refuse` — this command's own refusal, in its own words, at exit 2 — and the `exec` microseconds later, where tmux refuses the qualified target itself: `can't find window: @32`, exit 1. That is the fence doing its work and saying so, not an unexplained read failure; translating it would mean not replacing the process, which costs more than the code it would buy. * **`lane:944` — the population does not exist.** The finding is that an older `lanes-edit.sh` would reject `read_rows --lane` with 64 and die before the estate-wide fallback under `canon-lane`'s 2. Measured: `lanes)` and its `--lane)` arm landed in ONE commit, `63a74af` ("Lane tooling: restart, /restart, lanes, session-lane, window-lane, dir and profile in the record"), so every helper that has the `lanes` subcommand takes `--lane` — and one that does NOT have it returns the contract's 2 from `read_rows`, which is the "predates Amendment 11, take the install" refusal and is intended. There is no version in this repository's history between those two states. AND ROUND 8's `lane-start:698` IS ROUND 7's, TAKEN in `2c41f71` one commit before it read the tree. New cases: a lane whose recorded directory has a space in it (`repoSpace-1`), whose no-profile refusal is asserted to carry `cd $(printf %q)` and never the bare path; a no-terminal `lane --switch` listing carrying `lane --switch <name>` and never `lane-start --switch`; and a bare `lane-start --dir <path> --estate x` whose refusal names the refused flag AND the checkout it kept. Measured, on the merged tree (`origin/main` at a954435 is merged in): bash tests/test_lane_helpers.sh 1926 passed, 0 failed, 0 skipped, 0 pending Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * Copilot round 9, one taken and two declined — the retirement's printed `rm` is quoted the way both planners in that file already print theirs, and a `PAUSED` lane of another workstation is AVAILABLE here because that is the handoff this protocol is built on **`openRepoTools:281` — A PRINTED COMMAND IS A LINE SOMEBODY TYPES BACK.** `OPENREPOTOOLS_BIN_DIR` is a path a person chooses, and the retirement printed `rm <path>` bare — so a directory with a space in it produced a line that removes nothing, or removes something else. It is printed the way both planners in this same file already print theirs, which build their `paths` with the quotes IN: `rm -f -- "<path>"`, with `-f` for the reason those two give above their own line — what a removal needs is the DIRECTORY's write bit, not the file's. The three existing retirement cases assert that spelling now, and a new one runs the install with `OPENREPOTOOLS_BIN_DIR` set to `<tmp>/my bin dir`. **`lanes-edit.sh:4870` — DECLINED: a `PAUSED` lane is available on ANY workstation, and that is the act.** The finding is that `lane_groups` files a `PAUSED` row from Raven as AVAILABLE on Eagle rather than BOUND ELSEWHERE. It does, deliberately, and this pull request's own refusal says so to the person reading it: *"the exit is a person on <workstation> parking it — `/lane-swap` in that session — after which it is PARKED and `lane <lane>` takes it here"* (`go_elsewhere`). Routing a parked row to `elsewhere` would refuse the one case Addendum 2 retired `restart` for, and no parked lane could ever be taken to another machine. The fences that matter are all still in front of it: `go_available` refuses in EVERY state where this host's liveness read did not answer (round 4, `lane:560`); the recorded DIRECTORY must exist here by name, and the refusal says a lane parked elsewhere records that machine's path and that `resume <Name>` is what brings the work here; and a lane LIVE anywhere this host can see is `live` and never `available`. **`lane:1032` — DECLINED: `rg_list` carries TWO prepended fields.** The finding reads the loop's line as `lane-groups`' own output and calls the second `${rg_row#*$TAB}` a lane-name field being eaten. The line is this function's own list, built four lines above the read as `<repo> TAB <seq> TAB <the whole row>` — the two keys `sort -t"$TAB" -k1,1 -k2,2n` needs when no one repository is in scope — so two strips is exactly right. The suite holds it from both ends and would go red on that shift: the listing asserts `1 repoPick-1` with its STATE, profile and directory columns, and every numbered answer is asserted through to what it dispatched (`argv=--lane repoPick-1 team-05a`, `select-window -t picksess:@31`, `move-window -a -s detsess:@32`). All four CI jobs are green on `619c484`. Measured: python3 -m pytest tests/test_openrepotools_command.py \ tests/test_repo_hygiene.py tests/test_install_skill_and_hook.py -q 230 passed The shell suite is untouched by this commit (1926 passed on the tree it sits on, `e8aa021`), and the full `python3 -m pytest tests -q` under this workstation's lock covers both. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * Copilot round 10: the one thing it asked for that this branch did not already have is the CASE that makes round 9's decline durable — a `PAUSED` row of another workstation is AVAILABLE here, asserted so no reordering of that awk can take it away quietly Round 10 read `e8aa021`: three suppressed comments, no threads, and two of the three are round 9's `lanes-edit.sh:4870` twice more — that a `PAUSED` row recorded on Raven is grouped `available` on Eagle. It is, deliberately: that is clause (c)'s handoff and this pull request's own refusal says so to the person reading it, *"the exit is a person on <workstation> parking it — `/lane-swap` in that session — after which it is PARKED and `lane <lane>` takes it here"*. Declined on the thread, twice; and the third comment is the useful half of the same reading — **there was no case for it**, so a future reordering of those four lines would have turned the decline into a defect with nothing on screen. There is one now, fed straight to the helper: a PAUSED row of ANOTHER workstation is AVAILABLE available …while a BINDING of that same workstation is elsewhere Synthetic rows rather than a sixth seeded lane, because the six this section seeds ARE the partition every numbered pick below is counted against, and a seventh would renumber all of them. MEASURED BY HAND on this tree before the case was written, which is the same read the case makes: printf 'someRepo-9\tPAUSED\tRaven\n' | ./lanes-edit.sh lane-groups Eagle → available printf 'someRepo-9\tIDLE\tRaven\n' | ./lanes-edit.sh lane-groups Eagle → elsewhere printf 'someRepo-9\tPAUSED\tEagle\n' | ./lanes-edit.sh lane-groups Eagle → available AND ROUND 10's `lane:928` IS COVERAGE THIS BRANCH ALREADY HAS, one section up: `lane <name>` on an AVAILABLE lane is asserted through to the launcher's argv AND its cwd — "lane exits 0 for Evidence 7's lane once its directory is recorded" (`cwd=$HOME/projects/xFactory/xFactories/OpsxFactory`, `argv=--lane opsXfactory-5 team-03l`), with `lane <name> <profile>` beside it handing the launcher the profile that was NAMED and not the one recorded. The Amendment 18 Addendum 1 section is the pick and the three branches; the named AVAILABLE launch is `restart`'s own case, moved with the act under Addendum 2 and still asserted where it was written. The shell suite is 2095 passed, 0 failed on the tree this sits on (`e066be1`, `origin/main` merged at 2faa883); these two assertions make it 2097 and CI's own `tests` job measures them. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * Copilot round 11, two taken — the fence's own refusals offered `tmux attach -t <session>`, which is the wrong-lane outcome they exist to prevent, and a `canon-lane` read that FAILED was read as "the typed spelling will do" **`lane:499` and `lane:804` — A REFUSAL THAT OFFERS THE BARE SESSION OFFERS THE DEFECT.** Both ended with `tmux attach -t <session>`, which lands on whatever that session has since made current — another lane, which is the one thing the id-AND-session fence exists to stop. The window goes first now and the bare session stays under it, said as what it is: what the register says now: lanes --all that window, if it is back: tmux attach -t detsess:@32 that session, whatever it now holds: tmux attach -t detsess The qualified form is either the lane or a refusal, measured on tmux 3.4 at round 6: `attach -t dst:@2` attached AND made `@2` current in one act, and `attach -t dst:@3` for a window of another session refused with `can't find window: @3`. A person who wants the session anyway is still told how — knowing, from the words beside it, what they will get. **`lane:956` — TWO STATUSES ARE ANSWERS AND THE REST ARE A REFUSAL.** `0` is the row's own spelling; `2` WITHOUT the 15(d) words is the unknown-subcommand code of a helper predating Amendment 15, which is the documented fall-through to the estate-wide read below (that read matches case-insensitively, which is what makes the fallback work); `2` WITH them is `canon-lane`'s own two-rows refusal, relayed one branch up. Everything else was falling through to the TYPED spelling, which is a lane bound under a name nothing established — the defect Amendment 15 exists to prevent, and the fall-through Amendment 7(d) forbids. It refuses with 1 now, naming the code and the read to run by hand. `canon-lane` was the last read in this file without that rule; the row read, the grouping read and the position read have each had it since rounds 1 to 5. New cases: a select that FAILED outside tmux and a move that FAILED, each asserted to carry `tmux attach -t detsess:@32` and the "whatever it now holds" wording; a `canon-lane` exiting 5 → exit 1 with the code named and nothing launched; and a `canon-lane` unknown-subcommand 2 → the lane still binds through the launcher, because an un-upgraded workstation is not a fault. Measured on this tree (`origin/main` merged in at 2faa883): bash tests/test_lane_helpers.sh 2109 passed, 0 failed, 0 skipped, 0 pending and the full `python3 -m pytest tests -q` under this workstation's lock was **625 passed, 0 failed** in 1661 s on the tree twelve assertions ago. CI on `03171c3`: `tests`, `tests-no-submodule` and `tests-windows` green, `tests-macos` still running at the time of writing; all four were green on `b8dddc1` and on `619c484`. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * Copilot round 12, three taken — an answer wider than the shell's integers WRAPPED into a lane nobody picked, `f` told an un-upgraded workstation to read something that does not exist there, and `lane ""` answered a different question **`lane:1215` — A DIGIT STRING TOO LONG TO BE A POSITION IS NOT A POSITION.** `pick=$((10#$answer))` WRAPS at the shell's integer width: `18446744073709551617` comes back as `1`, so an answer far outside `1-N` reached the range check as a number that IS in it and would have bound a lane nobody picked. That is the wrong-lane outcome every fence in this file exists to stop, arriving through the one door that reads a person's typing. The LENGTH is compared before the conversion, against the digits of `$n` itself, because that is the widest answer the question offered. **`lane:873` — `2` IS THE CONTRACT'S OWN CODE IN THE `f` PATH TOO.** The listing has told the two statuses apart since round 3 — an un-upgraded workstation named as one and sent to `openRepoTools --install`, a read that FAILED named by its code — and this path, the same read one answer later, collapsed both into "run it by hand". A person who typed `f` on a workstation that has not taken the install was told to read a subcommand that does not exist there. **`lane:234` — AN ARGUMENT THAT WAS GIVEN AND IS EMPTY IS NOT "NO ARGUMENT".** `lane ""` left `$name` empty, every `[ -n "$name" ]` below it read that as "no lane was named", and a malformed named invocation silently became the LISTING — and on a terminal the numbered pick, a question the person did not ask. It is the contract's 64 at the door now, in the same terms the helper's own whole-string fence uses. AND THE FOURTH IS THIS PULL REQUEST'S OWN BODY: it said `--install` makes EIGHTEEN artifacts while the code's invariant became NINETEEN when `origin/main`'s #52 merged Amendment 12's `UserPromptSubmit` entry in beside the `SessionStart` one. Corrected there, and derived from `INSTALLED` and `HOOK_ENTRIES` rather than restated. New cases: `f` answered on a workstation whose helper has no `next-free` — refusing with 2, naming the install and starting nothing; the pick answered `18446744073709551617`, refused as outside `1-N` with nothing bound (it is the wrap that would have bound something); and `lane ""` and `lane --all ""`, each the contract's 64, neither of them the listing. Measured on this tree (`origin/main` merged in at 2faa883): bash tests/test_lane_helpers.sh 2120 passed, 0 failed, 0 skipped, 0 pending CI is green on the head this sits on — `42d243a`, all four jobs, `tests-macos` included — as it was on `03171c3` and on `619c484` before it. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m * The suite's own wrapper is what failed on macOS, not a case in it: 2345 assertions take 45 minutes there and the bound was 40, so `TIMEOUT_SECONDS` goes 2400 → 3600 — measured, and with the real fix filed as #77 MEASURED, `b9018c1`, this branch with Amendment 17's suite section merged beside Amendment 18 Addendum 1's — 2102 cases to 2345: tests-macos 03:18:03 → 04:03:37 1 failed, 654 passed in 2722.13s (0:45:22) subprocess.TimeoutExpired: Command '['bash', '…/tests/test_lane_helpers.sh']' timed out after 2400 seconds tests 03:17:59 → 03:34:46 green, the same 655 cases (16m 47s) tests-no-submodule 24m 53s green · tests-windows 26s green **EVERY OTHER CASE IN THAT macOS JOB PASSED.** What the bound caught is the suite being long on the slowest platform it is required to pass on — macOS at roughly 2.7× the Linux time for the same file — and not a helper hanging, which is the thing the bound exists for and still does at 3600. A `TimeoutExpired` says nothing about which assertion was in flight, which is exactly the unreadable failure the paragraph above that number already warns about: it is why the same number went 900 → 2400 one amendment ago, for the same reason, and the comment now carries both rounds. ONE NUMBER AND NOT A PER-PLATFORM PAIR. A bound that is different where a person is not looking is a bound nobody can reason about, and macOS is not the only slow place: a legitimate run of this same tree took **2129 s on this workstation** with six sibling suites building beside it, which is inside 2400 by four minutes and outside it on a busier evening. The job has no `timeout-minutes` in `.github/workflows/tests.yml`, so nothing under it bites before this does — GitHub's own default is 360 minutes, ten times this. THE CAP IS NOT THE FIX. One bash file running real `git` several thousand times serially for 45 minutes is the fix, and it is **opensoft/openRepoTools#77**, filed and claimed by this lane with what to measure first — where the time actually goes, before choosing between a per-section sandbox, parallelism the wrapper's one-test shape would have to give up, and the cheap `git` settings in the sandbox's own config. This commit moves the cap out of that work's way and decides nothing about it. No assertion is touched and no case is skipped. Lane: openrepotools-3 (openRepoTools-3) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UFwD7bCS73jaUMJ2vuo5m --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>



Lane: openRepoTools-3
← brettheap/new-workstation#28 (Amendment 15, ratified 2026-09-14T00:07:18Z)
A lane name is compared CASE-INSENSITIVELY wherever a name is looked up, and
the spelling the register row carries is canonical. At 2026-09-13T23:51:56Z a
lane-start openxfactory 2, typed in lowercase for the laneopenXfactory-2that had run since 2026-09-02, found NO row — the row key was the one exact
comparison left in the tooling, while
repos.tsv,lane_named_ci, Rule 6attribution and home matching already lowercased — minted a new session, named
the window
openxfactory-2and appended a SECOND row, while its object log wenton into
lanes/log/openxfactory-2.md, the one file both spellings had alwaysshared. Two rows, one lane, one log. This is the tooling that makes that pair
impossible.
The six acts
1 —
lanes-edit.sh canon_lane(:2767). The register row whose key equalsthe typed name case-insensitively, read from
origin/<branch>like every otherstate read (R19): exactly one row prints its OWN spelling, none prints the typed
spelling so
add-rowcan still create a lane, two or more REFUSE (2) namingthem all and citing 15(d). It RETURNS rather than exits, for
row_line's reason— every caller takes it with
$( ). Exposed as the subcommandcanon-lane(
:6155) so the four commands share ONE resolver. Every<lane>argument andLANES_LANEpasses through it before any read or write —log,claim,release,who --lane,live-holder,idle-holders,forks,lane-objects,register-row,lane-dir,lane-profile,last-session,lanes --lane,append-session-id,append-row-status,replace-in-row,add-row,commit,append-line,verify-row,resolve-home— so the lines written, the commitsubjects
LANES(<lane>@WS)/LOG(<lane>@WS)andcanon_object'slane:<name>key all carry the canonical spelling.
row_line,ROW_AWKandrows_named_cicomparetolower; so do the OBJECT LOG's own readers —lane_objects,superseded_by,claim_is_stale,releaseandlane_row_facts— because a log is append-only and its lane field carrieswhatever was typed on the day. The log FILE is
lanes/log/<canonical>.md:readers find a file of another case, and the first write renames it once,
through a temporary name so a case-insensitive filesystem renames rather than
answering "identical". That write then drops the OLD path from the INDEX by its
exact name, adds the new one, and commits the INDEX rather than its pathspecs —
the one sequence that records the rename on a case-insensitive filesystem too,
argued in full above
commit_pushand measured three times on this PR's macOSjob (below). Amendment 12's guard and lock are NOT
built here (#25); the one line marking where they plug in is in
session_start_block.2 —
lane-start(:671,:721).<repo>resolves to the checkout's realdirectory name under
$PROJECTS_ROOTcase-insensitively, two directoriesdiffering only by case refusing and naming both; the lane resolves through
canon-lane, which wins over the directory and over what was typed;rows_for_lanecomparestolower, so the pre-check RESUMES the row it used tomiss instead of reaching
add-row, and the two-rows refusal says whichspellings and cites 15(d). The window rename and every
--nametake thecanonical spelling because
$LANEis it. A NEW lane's<repo>half is thedirectory's real name. And the transcript TITLE is matched case-insensitively
too (
:1639), in the one branch that decides RESUME versus MINT A NEWCONVERSATION:
openXfactory-2is in exactly that state right now — its sessionwas titled
openxfactory-2by the launcher on 2026-09-13 and its row has saidopenXfactory-2since the 15(d) merge — so a byte-for-byte matcher would findno transcript for the lane and start a second conversation on it, which is the
incident itself, one branch along. The note below it is three answers now
instead of two (
:1696): a dedupe suffix is another holder live at the rename,and a title differing only by case is this lane's own transcript with nothing to
retire.
3 —
lane-end,lanes,restart. The same resolver on their<lane>argument;
restart's andlane-start'sawk '$1 == l'on the swap recordcompare
tolower;lanes's--lanegoes through the resolver and its--prefixfilter already lowercased both sides. And the shared resolver isnot the whole of it:
canon-lanespends2both on 15(d)'s pair and as theunknown-subcommand code of every helper predating this amendment, so
lane-endand
restartcannot tell those apart and carry on with the name AS TYPED — whateach command's own read of the register then says is the answer a person gets.
lane-end'srows_for_lanewas still byte for byte (:651), under the samefunction name whose twin in
lane-startthis branch had already madetolower:lane-end openxfactory-2on the laneopenXfactory-2read as NO ROW, and thatrefusal's own remedy is
add-row— the incident, performed by the other end ofthe lane. Both sides lowered, and its two-rows refusal now names the spellings
and cites 15(d) (
:679).4 — the hook and the two window/session reads.
session_start_blockprints$ssb_lanein every message and every filled-in command, and both of its readsanswer with the register's spelling (
lane_named_cilowercases both sides,lane_of_sessionprints the row's token).--confirm's prompt no longer callsa window named for this lane in another case "not the lane"
(
lane-start:1508) — the rename happens either way, and what a wrong sentencechanges is the ANSWER a person gives it.
window-lane's rung 2 — the swapRECORD, whose lane column carries whatever the lane was PAUSED under — now goes
through
canon_lanetoo, because what that read prints is handed totmux rename-window, to--nameand torestart <lane>by its three callers.5 — the suite. A section
== Amendment 15: a lane name is one name under any case ==, immediately before== the workstation seam ==, which stayssecond-to-last. It seeds the incident's own shape and asserts, against the real
commands: a lowercase
lane-start repocase 1RESUMES the mixed-case row, is--named and renames the window with the row's spelling, and adds no secondrow; a row spelled unlike its checkout (
repoMixon disk,repoMIX-1in theregister) takes the ROW's spelling for the lane and the DIRECTORY's for its
dir; a lane whose transcript is TITLED in the other case is resumed ratherthan replaced, and the note says so instead of calling it a dedupe suffix; the
log file is renamed to the row's spelling ONCE, inside the write's
own commit, which names both paths;
add-rowrefuses the case-duplicate namingthe row that is there;
log,claimandappend-session-ideach refuse theseeded 15(d) pair naming both spellings, and so does
lane-endout of its ownread of the register rather than through the helper; a repo directory typed in
the wrong case is found and two differing only by case refuse (skipped, not asserted
vacuously, on a case-insensitive filesystem); Rule 6 attribution with a
lowercase
Lane:still matches and commits under the row's spelling; a CLAIMEDwritten in the OTHER case in a lane's own log is still that lane's hold, so
who --lanenames it andlane-endREFUSES to end the lane; a log that spellsits lane both ways yields ONE set of facts, so column 10 of
lanes --laneis arestart line at all;
window-laneanswers with the row's spelling from a swaprecord that spells it another way; and
who --lane,lanes --lane,register-row,canon-lane,restartandlane-endall take the lowercasespelling.
6 — the manual.
docs/README-lanes.md: the naming rule and canonicalspelling under "Starting and ending a lane", the writer's first act,
lane-start <repo> <n>'s two resolutions and its step 5add-row, the object log's filename and its lane field, and
window-lane's row in the reads table. AGENTS.mdis untouched, and that is the reading rather than an omission: its lane
paragraph says where the four commands find the register and whose exit codes
they spend, and this amendment changes neither — so its 224-line cap in
tests/test_repo_hygiene.pydoes not move.docs/README-lanes.mdcarries nocap.
Three things found in acts 1–4 and fixed here
A refusal that would have killed
lane-starton macOS. The two-rows refusalin
lane-startread"spelled $rfl_names— a lane name is ONE name…". Bashdecides where a variable NAME ends with
isalnum(), which isLOCALE-DEPENDENT: bash 3.2 on macOS, under the runner's
en_US.UTF-8, readsthe three bytes of the em dash as name characters, looks up
rfl_names—, andset -ukills the script — in the one branch whose whole job is to explain aregister a person has to repair by hand.
bash -ncannot see it and neither cana Linux run;
test_no_shipped_bash_leaves_a_variable_name_to_bash_3_2s_localedid, the first time this branch was put in front of it. Braced
(
lane-start:1111).An empty array under
set -uon bash 3.2.lanes --lane's resolver looptook
${#lns_args[@]}and an index; a barelaneswith no flags is the emptycase that
${arr[@]+"${arr[@]}"}exists for — the idiom thelanes_rowscallone line below already uses. Rebuilt with it, and it no longer assumes the
--lanevalue sits ati + 1either.A refusal that could not be acted on.
ensure_log's rename said "move itby hand" without saying which path. Its two
mvs fail differently: the firstleaves the log exactly where it was, the SECOND leaves it at
<log>.amendment15.<pid>, a name no reader of that directory looks for. Splitin two, each naming where the file actually is. (And the manual's
git mviscorrected to what the code does and says why:
git mv a Ais not a rename on acase-insensitive filesystem.)
Measured
On Linux, a case-SENSITIVE filesystem, so the two-directories case ran rather
than skipping.
bash tests/test_lane_helpers.shon the inherited WIP: 1563 passed, 1failed, 0 skipped, 0 pending. The one failure was an assertion that was
wrong about the resolution ORDER — corrected here, and the case where only the
register can settle the spelling added beside it.
bash tests/test_lane_helpers.shon this branch: 1618 passed, 0 failed, 0skipped, 0 pending.
python3 -m pytest tests -q: 549 passed in 866 s — the whole suite,including a third run of the bash one through
tests/test_lane_helpers_suite.py, which asserts its footer says0 failed.The macOS job, which is the case-INSENSITIVE half
This repository's CI runs the suite on macOS, and the case-only rename is the
one act whose meaning changes there. Three rounds, each measured rather than
guessed:
48f2111—git add -- <old> <new>under git's owncore.ignorecase=truematches both pathspecs to the ONE index entry and stages a MODIFICATION under
the old name: fifteen red,
git log -- <canonical>came back EMPTY.e35f2a8and928908a— the sameaddundercore.ignorecase=falseisworse. Exact matching changes what GIT compares, not what the OPERATING
SYSTEM resolves, so
lstaton the old path still finds the renamed file: gitstages the old entry as MODIFIED and the new path as a SECOND entry, and the
commit carries both. ONE file on disk, TWO paths in the tree — a lane with
two committed logs, the state this amendment exists to make impossible. Every
checkout after it reports the path it cannot materialise as deleted, so the
writes behind it are refused (2) for a dirty checkout they did not make:
eleven red, all of them behind
lanes/log/repocase-1.mdreported dirty.617d383— the INDEX is the only place a case-only rename can be spelledexactly, and
git commit -- <paths>is--only(it rebuilds the tree fromHEAD and the WORKING TREE and disregards what is staged), so it must be
committed FROM the index. Fail-closed: anything staged that is not this
write's own pathspec REFUSES (6) and names it, rather than riding along under
this lane's subject. Only the write that carries a rename takes that branch.
And the two assertions that watched the rename —
git show --name-statusnamesthe old path, and names the new one — BOTH pass on that pathology, so they were
green on macOS while the tree held two logs for one lane. What names it on any
filesystem is the tree itself:
git ls-tree -r HEAD -- lanes/logmust hold ONEpath for the lane's log, counted case-insensitively so that a second spelling
is a second path. Asserted now for
repoCase-1and forrepoIgn-1.No assertion was weakened or deleted; two were added.
Closes #31
Lane: openrepotools-3 (openRepoTools-3)
🤖 Generated with Claude Code
Summary by Sourcery
Make lane names case-insensitive throughout the tooling while enforcing the register row’s spelling as canonical and keeping each lane’s register entry and object log unambiguous.
New Features:
canon-lanesubcommand.Bug Fixes:
Enhancements:
LANES_LANEattribution and commit subjects.Documentation:
Tests: